diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 100644 index 00000000..a3b6e7d3 --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1,411 @@ +# 33 JavaScript Concepts - Project Context + +## Overview + +This repository is a curated collection of **33 essential JavaScript concepts** that every JavaScript developer should know. It serves as a comprehensive learning resource and study guide for developers at all levels, from beginners to advanced practitioners. + +The project was recognized by GitHub as one of the **top open source projects of 2018** and has been translated into 40+ languages by the community. + +## Project Purpose + +- Help developers master fundamental and advanced JavaScript concepts +- Provide curated resources (articles, videos, books) for each concept +- Serve as a reference guide for interview preparation +- Foster community contributions through translations and resource additions + +## Repository Structure + +``` +33-js-concepts/ +├── .claude/ # Claude configuration +│ └── CLAUDE.md # Project context and guidelines +├── .opencode/ # OpenCode configuration +│ └── skill/ # Custom skills for content creation +│ └── write-concept/ # Skill for writing concept documentation +├── docs/ # Mintlify documentation site +│ ├── docs.json # Mintlify configuration +│ ├── index.mdx # Homepage +│ ├── introduction.mdx # Getting started guide +│ ├── contributing.mdx # Contribution guidelines +│ ├── translations.mdx # Community translations +│ └── concepts/ # 33 concept pages +│ ├── call-stack.mdx +│ ├── primitive-types.mdx +│ └── ... (all 33 concepts) +├── tests/ # Vitest test suites +│ └── fundamentals/ # Tests for fundamental concepts (1-6) +│ ├── call-stack/ +│ ├── primitive-types/ +│ ├── value-reference-types/ +│ ├── type-coercion/ +│ ├── equality-operators/ +│ └── scope-and-closures/ +├── vitest.config.js # Vitest configuration +├── README.md # Main GitHub README +├── CONTRIBUTING.md # Guidelines for contributors +├── CODE_OF_CONDUCT.md # Community standards +├── LICENSE # MIT License +├── package.json # Project metadata +├── opencode.json # OpenCode AI assistant configuration +└── github-image.png # Project banner image +``` + +## The 31 Concepts (32nd and 33rd coming soon) + +### Fundamentals (1-6) +1. Primitive Types +2. Value Types and Reference Types +3. Type Coercion (Implicit, Explicit, Nominal, Structuring and Duck Typing) +4. Equality Operators (== vs === vs typeof) +5. Scope & Closures +6. Call Stack + +### Functions & Execution (7-8) +7. Event Loop (Message Queue) +8. IIFE, Modules and Namespaces + +### Web Platform (9-10) +9. DOM and Layout Trees +10. HTTP & Fetch + +### Object-Oriented JS (11-15) +11. Factories and Classes +12. this, call, apply and bind +13. new, Constructor, instanceof and Instances +14. Prototype Inheritance and Prototype Chain +15. Object.create and Object.assign + +### Functional Programming (16-19) +16. map, reduce, filter +17. Pure Functions, Side Effects, State Mutation and Event Propagation +18. Higher-Order Functions +19. Recursion + +### Async JavaScript (20-22) +20. Collections and Generators +21. Promises +22. async/await + +### Advanced Topics (23-31) +23. JavaScript Engines +24. Data Structures +25. Big O Notation (Expensive Operations) +26. Algorithms +27. Inheritance, Polymorphism and Code Reuse +28. Design Patterns +29. Partial Applications, Currying, Compose and Pipe +30. Clean Code + +## Content Format + +Each concept page in `/docs/concepts/` follows this structure: + +### 1. Frontmatter +```mdx +--- +title: "Concept Name" +description: "Brief description of the concept" +--- +``` + +### 2. Real-World Analogy +Start with an engaging analogy that makes the concept relatable. Include ASCII art diagrams when helpful. + +### 3. Info Box (What You'll Learn) +```mdx + +**What you'll learn in this guide:** +- Key point 1 +- Key point 2 +- Key point 3 + +``` + +### 4. Main Content Sections +- Use clear headings (`##`, `###`) to organize topics +- Include code examples with explanations +- Use Mintlify components (``, ``, ``, etc.) +- Add diagrams and visualizations where helpful + +### 5. Related Concepts +```mdx + + + Brief description of how it relates + + +``` + +### 6. Reference +```mdx + + Official MDN documentation + +``` + +### 7. Articles +Curated blog posts and tutorials using `` with `icon="newspaper"`. + +### 8. Courses (optional) +Educational courses using `` with `icon="graduation-cap"`. + +### 9. Videos +YouTube tutorials and conference talks using `` with `icon="video"`. + +## Contributing Guidelines + +### Adding Resources +- Resources should be high-quality and educational +- Follow the existing Card format for consistency +- Include a brief description of what the resource covers + +### Resource Format +```mdx + + Brief description of what the reader will learn from this resource. + +``` + +## Git Commit Conventions + +This project follows the [Conventional Commits](https://www.conventionalcommits.org/) specification. All commits must adhere to this format for consistency and automated changelog generation. + +### Commit Message Format + +``` +[optional scope]: + +[optional body] + +[optional footer(s)] +``` + +### Commit Types + +| Type | Description | +|------|-------------| +| `feat` | New features or content additions (e.g., new resources, new concepts) | +| `fix` | Bug fixes, broken link corrections, typo fixes | +| `docs` | Documentation changes (README updates, CONTRIBUTING updates) | +| `style` | Formatting changes (markdown formatting, whitespace) | +| `refactor` | Content restructuring without adding new resources | +| `chore` | Maintenance tasks (config updates, dependency updates) | +| `ci` | CI/CD configuration changes | +| `perf` | Performance improvements | +| `test` | Adding or updating tests | +| `build` | Build system or external dependency changes | +| `revert` | Reverting a previous commit | + +### Examples + +```bash +# Adding a new resource +feat: add article about closures by John Doe + +# Fixing a broken link +fix: update broken MDN link in Promises section + +# Documentation update +docs: update contributing guidelines for translations + +# Maintenance task +chore: update opencode.json configuration + +# Adding content to existing concept +feat(closures): add video tutorial by Fun Fun Function + +# Multiple changes in body +feat: add new resources for async/await + +- Add article by JavaScript Teacher +- Add video tutorial by Traversy Media +- Update reference links +``` + +### Rules + +1. **Use lowercase** for the type and description +2. **No period** at the end of the description +3. **Use imperative mood** ("add" not "added", "fix" not "fixed") +4. **Keep the first line under 72 characters** +5. **Reference issues** in the footer when applicable (e.g., `Closes #123`) + +## MCP Servers Available + +This project has OpenCode configured with: + +1. **Context7** - Documentation search (`use context7` in prompts) +2. **GitHub** - Repository management (`use github` in prompts) + +## Testing + +This project uses [Vitest](https://vitest.dev/) as the test runner to verify that code examples in the documentation work correctly. + +### Running Tests + +```bash +# Run all tests once +npm test + +# Run tests in watch mode (re-runs on file changes) +npm run test:watch + +# Run tests with coverage report +npm run test:coverage +``` + +### Test Structure + +Tests are organized by concept category in the `tests/` directory: + +``` +tests/ +├── fundamentals/ # Concepts 1-6 +│ ├── call-stack/ +│ ├── primitive-types/ +│ ├── value-reference-types/ +│ ├── type-coercion/ +│ ├── equality-operators/ +│ └── scope-and-closures/ +├── functions-execution/ # Concepts 7-8 +│ ├── event-loop/ +│ └── iife-modules/ +└── web-platform/ # Concepts 9-10 + ├── dom/ + └── http-fetch/ +``` + +### Writing Tests for Code Examples + +When adding new code examples to concept documentation, please include corresponding tests: + +1. **File naming**: Create `{concept-name}.test.js` in `tests/{category}/{concept-name}/` +2. **Use explicit imports**: + ```javascript + import { describe, it, expect } from 'vitest' + ``` +3. **Convert console.log examples to assertions**: + ```javascript + // Documentation example: + // console.log(typeof "hello") // "string" + + // Test: + it('should return string type', () => { + expect(typeof "hello").toBe("string") + }) + ``` +4. **Test error cases**: Use `expect(() => { ... }).toThrow()` for operations that should throw +5. **Skip browser-specific examples**: Tests run in Node.js, so skip DOM/window/document examples +6. **Note strict mode behavior**: Vitest runs in strict mode, so operations that "silently fail" in non-strict mode will throw `TypeError` + +### Current Test Coverage + +| Category | Concept | Tests | +|----------|---------|-------| +| Fundamentals | Call Stack | 20 | +| Fundamentals | Primitive Types | 73 | +| Fundamentals | Value vs Reference Types | 54 | +| Fundamentals | Type Coercion | 74 | +| Fundamentals | Equality Operators | 87 | +| Fundamentals | Scope and Closures | 46 | +| Functions & Execution | Event Loop | 56 | +| Functions & Execution | IIFE & Modules | 61 | +| Web Platform | DOM | 85 | +| Web Platform | HTTP & Fetch | 72 | +| **Total** | | **628** | + +## Documentation Site (Mintlify) + +The project includes a Mintlify documentation site in the `/docs` directory. + +### Local Development + +```bash +# Using npm script +npm run docs + +# Or install Mintlify CLI globally +npm i -g mint +cd docs +mint dev +``` + +The site will be available at `http://localhost:3000`. + +### Documentation Structure + +- **Getting Started**: Homepage and introduction +- **Fundamentals**: Concepts 1-6 (Primitive Types through Call Stack) +- **Functions & Execution**: Concepts 7-8 (Event Loop through IIFE/Modules) +- **Web Platform**: Concepts 9-10 (DOM and HTTP & Fetch) +- **Object-Oriented JS**: Concepts 11-15 (Factories through Object.create/assign) +- **Functional Programming**: Concepts 16-19 (map/reduce/filter through Recursion) +- **Async JavaScript**: Concepts 20-22 (Collections/Generators through async/await) +- **Advanced Topics**: Concepts 23-31 (JavaScript Engines through Clean Code) + +### Adding/Editing Concept Pages + +Each concept page is in `docs/concepts/` and follows this template: + +```mdx +--- +title: "Concept Name" +description: "Brief description" +--- + +## Overview +[Explanation of the concept] + +## Reference +[MDN or official docs links] + +## Articles +[Curated articles with CardGroup components] + +## Videos +[Curated videos with CardGroup components] +``` + +## Important Notes + +- This is primarily a documentation/resource repository, not a code library +- The main content lives in `README.md` and `/docs` (Mintlify site) +- Translations are maintained in separate forked repositories +- Community contributions are welcome and encouraged +- MIT Licensed + +## Custom Skills + +### write-concept Skill + +Use the `/write-concept` skill when writing or improving concept documentation pages. This skill provides comprehensive guidelines for: + +- **Page Structure**: Exact template for concept pages (frontmatter, opening hook, code examples, sections) +- **SEO Optimization**: Critical guidelines for ranking in search results +- **Writing Style**: Voice, tone, and how to make content accessible to beginners +- **Code Examples**: Best practices for clear, educational code +- **Quality Checklists**: Verification steps before publishing + +**When to invoke:** +- Creating a new concept page in `/docs/concepts/` +- Rewriting or significantly improving an existing concept page +- Reviewing an existing concept page for quality + +**SEO is Critical:** Each concept page should rank for searches like: +- "what is [concept] in JavaScript" +- "how does [concept] work in JavaScript" +- "[concept] JavaScript explained" + +The skill includes detailed guidance on title optimization (50-60 chars), meta descriptions (150-160 chars), keyword placement, and featured snippet optimization. + +**Location:** `.opencode/skill/write-concept/SKILL.md` + +## Maintainer + +**Leonardo Maldonado** - [@leonardomso](https://github.com/leonardomso) + +## Links + +- Repository: https://github.com/leonardomso/33-js-concepts +- Issues: https://github.com/leonardomso/33-js-concepts/issues +- Original Article: [33 Fundamentals Every JavaScript Developer Should Know](https://medium.com/@stephenthecurt/33-fundamentals-every-javascript-developer-should-know-13dd720a90d1) by Stephen Curtis diff --git a/.github/workflows/action.yml b/.github/workflows/action.yml deleted file mode 100644 index 2c3a04d1..00000000 --- a/.github/workflows/action.yml +++ /dev/null @@ -1,10 +0,0 @@ -name: Check Markdown links - -on: push - -jobs: - markdown-link-check: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@master - - uses: gaurav-nelson/github-action-markdown-link-check@v1 \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..718f1abf --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,27 @@ +name: Tests + +on: + push: + branches: [master, main] + pull_request: + branches: [master, main] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 'lts/*' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm test diff --git a/.gitignore b/.gitignore index 39ecea19..0cc13e70 100644 --- a/.gitignore +++ b/.gitignore @@ -62,3 +62,6 @@ typings/ # webstore IDE created directory .idea + +# OpenCode configuration (local only) +.opencode/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 95a960d4..9cd591c0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,59 @@ # Contribution This project would not be possible without your help and support, and we appreciate your willingness to contribute! +## Testing + +This project uses [Vitest](https://vitest.dev/) as the test runner to verify that code examples in the documentation work correctly. + +### Running Tests + +```bash +# Run all tests once +npm test + +# Run tests in watch mode (re-runs on file changes) +npm run test:watch + +# Run tests with coverage report +npm run test:coverage +``` + +### Test Structure + +Tests are organized by concept in the `tests/` directory: + +``` +tests/ +├── call-stack/ +│ └── call-stack.test.js +├── primitive-types/ +│ └── primitive-types.test.js +└── ... +``` + +### Writing Tests for Code Examples + +When adding new code examples to concept documentation, please include corresponding tests: + +1. **File naming**: Create `{concept-name}.test.js` in `tests/{concept-name}/` +2. **Use explicit imports**: + ```javascript + import { describe, it, expect } from 'vitest' + ``` +3. **Convert console.log examples to assertions**: + ```javascript + // Documentation example: + // console.log(typeof "hello") // "string" + + // Test: + it('should return string type', () => { + expect(typeof "hello").toBe("string") + }) + ``` +4. **Test error cases**: Use `expect(() => { ... }).toThrow()` for operations that should throw +5. **Skip browser-specific examples**: Tests run in Node.js, so skip DOM/window/document examples +6. **Note strict mode behavior**: Vitest runs in strict mode, so operations that "silently fail" in non-strict mode will throw `TypeError` + ### Creating a New Translation To create a new translation, please follow these steps: diff --git a/README.md b/README.md index 924a02f7..95541bbc 100644 --- a/README.md +++ b/README.md @@ -1,1292 +1,170 @@


- 33 Concepts Every JS Developer Should Know + 33 Concepts Every JS Developer Should Know

33 Concepts Every JavaScript Developer Should Know

-
-

- Introduction • - Community • - Table of Contents • - License -

-
+

+ Read the Full Guide • + Concepts • + Translations • + Contributing +

- 🚀 Considered by GitHub as one of the top open source projects of 2018! + Recognized by GitHub as one of the top open source projects of 2018!
-## Introduction - -This repository was created with the intention of helping developers master their concepts in JavaScript. It is not a requirement, but a guide for future studies. It is based on an article written by Stephen Curtis and you can read it [here](https://medium.com/@stephenthecurt/33-fundamentals-every-javascript-developer-should-know-13dd720a90d1). - -## Community - -Feel free to submit a PR by adding a link to your own recaps or reviews. If you want to translate the repo into your native language, please feel free to do so. - -All the translations for this repo will be listed below: - -- [اَلْعَرَبِيَّةُ‎ (Arabic)](https://github.com/amrsekilly/33-js-concepts) — Amr Elsekilly -- [Български (Bulgarian)](https://github.com/thewebmasterp/33-js-concepts) - thewebmasterp -- [汉语 (Chinese)](https://github.com/stephentian/33-js-concepts) — Re Tian -- [Português do Brasil (Brazilian Portuguese)](https://github.com/tiagoboeing/33-js-concepts) — Tiago Boeing -- [한국어 (Korean)](https://github.com/yjs03057/33-js-concepts.git) — Suin Lee -- [Español (Spanish)](https://github.com/adonismendozaperez/33-js-conceptos) — Adonis Mendoza -- [Türkçe (Turkish)](https://github.com/ilker0/33-js-concepts) — İlker Demir -- [русский язык (Russian)](https://github.com/gumennii/33-js-concepts) — Mihail Gumennii -- [Tiếng Việt (Vietnamese)](https://github.com/nguyentranchung/33-js-concepts) — Nguyễn Trần Chung -- [Polski (Polish)](https://github.com/lip3k/33-js-concepts) — Dawid Lipinski -- [فارسی (Persian)](https://github.com/majidalavizadeh/33-js-concepts) — Majid Alavizadeh -- [Bahasa Indonesia (Indonesian)](https://github.com/rijdz/33-js-concepts) — Rijdzuan Sampoerna -- [Français (French)](https://github.com/robinmetral/33-concepts-js) — Robin Métral -- [हिन्दी (Hindi)](https://github.com/vikaschauhan/33-js-concepts) — Vikas Chauhan -- [Ελληνικά (Greek)](https://github.com/DimitrisZx/33-js-concepts) — Dimitris Zarachanis -- [日本語 (Japanese)](https://github.com/oimo23/33-js-concepts) — oimo23 -- [Deutsch (German)](https://github.com/burhannn/33-js-concepts) — burhannn -- [украї́нська мо́ва (Ukrainian)](https://github.com/AndrewSavetchuk/33-js-concepts-ukrainian-translation) — Andrew Savetchuk -- [සිංහල (Sinhala)](https://github.com/ududsha/33-js-concepts) — Udaya Shamendra -- [Italiano (Italian)](https://github.com/Donearm/33-js-concepts) — Gianluca Fiore -- [Latviešu (Latvian)](https://github.com/ANormalStick/33-js-concepts) - Jānis Īvāns -- [Afaan Oromoo (Oromo)](https://github.com/Amandagne/33-js-concepts) - Amanuel Dagnachew -- [ภาษาไทย (Thai)](https://github.com/ninearif/33-js-concepts) — Arif Waram -- [Català (Catalan)](https://github.com/marioestradaf/33-js-concepts) — Mario Estrada -- [Svenska (Swedish)](https://github.com/FenixHongell/33-js-concepts/) — Fenix Hongell -- [ខ្មែរ (Khmer)](https://github.com/Chhunneng/33-js-concepts) — Chrea Chanchhunneng -- [አማርኛ (Ethiopian)](https://github.com/hmhard/33-js-concepts) - Miniyahil Kebede(ምንያህል ከበደ) -- [Беларуская мова (Belarussian)](https://github.com/Yafimau/33-js-concepts) — Dzianis Yafimau -- [O'zbekcha (Uzbek)](https://github.com/smnv-shokh/33-js-concepts) — Shokhrukh Usmonov -- [Urdu (اردو)](https://github.com/sudoyasir/33-js-concepts) — Yasir Nawaz -- [हिन्दी (Hindi)](https://github.com/milostivyy/33-js-concepts) — Mahima Chauhan -- [বাংলা (Bengali)](https://github.com/Jisan-mia/33-js-concepts) — Jisan Mia -- [ગુજરાતી (Gujarati)](https://github.com/VatsalBhuva11/33-js-concepts) — Vatsal Bhuva -- [سنڌي (Sindhi)](https://github.com/Sunny-unik/33-js-concepts) — Sunny Gandhwani -- [भोजपुरी (Bhojpuri)](https://github.com/debnath003/33-js-concepts) — Pronay Debnath -- [ਪੰਜਾਬੀ (Punjabi)](https://github.com/Harshdev098/33-js-concepts) — Harsh Dev Pathak -- [Latin (Latin)](https://github.com/Harshdev098/33-js-concepts) — Harsh Dev Pathak -- [മലയാളം (Malayalam)](https://github.com/Stark-Akshay/33-js-concepts) — Akshay Manoj -- [Yorùbá (Yoruba)](https://github.com/ayobaj/33-js-concepts) - Ayomide Bajulaye -- [עברית‎ (Hebrew)](https://github.com/rafyzg/33-js-concepts) — Refael Yzgea -- [Nederlands (Dutch)](https://github.com/dlvisser/33-js-concepts) — Dave Visser -- [தமிழ் (Tamil)] (https://github.com/UdayaKrishnanM/33-js-concepts) - Udaya Krishnan M - -
- -## Table of Contents - -1. [**Call Stack**](#1-call-stack) -2. [**Primitive Types**](#2-primitive-types) -3. [**Value Types and Reference Types**](#3-value-types-and-reference-types) -4. [**Implicit, Explicit, Nominal, Structuring and Duck Typing**](#4-implicit-explicit-nominal-structuring-and-duck-typing) -5. [**== vs === vs typeof**](#5--vs--vs-typeof) -6. [**Function Scope, Block Scope and Lexical Scope**](#6-function-scope-block-scope-and-lexical-scope) -7. [**Expression vs Statement**](#7-expression-vs-statement) -8. [**IIFE, Modules and Namespaces**](#8-iife-modules-and-namespaces) -9. [**Message Queue and Event Loop**](#9-message-queue-and-event-loop) -10. [**setTimeout, setInterval and requestAnimationFrame**](#10-settimeout-setinterval-and-requestanimationframe) -11. [**JavaScript Engines**](#11-javascript-engines) -12. [**Bitwise Operators, Type Arrays and Array Buffers**](#12-bitwise-operators-type-arrays-and-array-buffers) -13. [**DOM and Layout Trees**](#13-dom-and-layout-trees) -14. [**Factories and Classes**](#14-factories-and-classes) -15. [**this, call, apply and bind**](#15-this-call-apply-and-bind) -16. [**new, Constructor, instanceof and Instances**](#16-new-constructor-instanceof-and-instances) -17. [**Prototype Inheritance and Prototype Chain**](#17-prototype-inheritance-and-prototype-chain) -18. [**Object.create and Object.assign**](#18-objectcreate-and-objectassign) -19. [**map, reduce, filter**](#19-map-reduce-filter) -20. [**Pure Functions, Side Effects, State Mutation and Event Propagation**](#20-pure-functions-side-effects-state-mutation-and-event-propagation) -21. [**Closures**](#21-closures) -22. [**High Order Functions**](#22-high-order-functions) -23. [**Recursion**](#23-recursion) -24. [**Collections and Generators**](#24-collections-and-generators) -25. [**Promises**](#25-promises) -26. [**async/await**](#26-asyncawait) -27. [**Data Structures**](#27-data-structures) -28. [**Expensive Operation and Big O Notation**](#28-expensive-operation-and-big-o-notation) -29. [**Algorithms**](#29-algorithms) -30. [**Inheritance, Polymorphism and Code Reuse**](#30-inheritance-polymorphism-and-code-reuse) -31. [**Design Patterns**](#31-design-patterns) -32. [**Partial Applications, Currying, Compose and Pipe**](#32-partial-applications-currying-compose-and-pipe) -33. [**Clean Code**](#33-clean-code) - -
- -## 1. Call Stack - -

The call stack is a mechanism that the JavaScript interpreter uses to keep track of function execution within a program. In JavaScript, functions are executed in the order they are called. The call stack follows the Last In, First Out (LIFO) principle, meaning that the last function pushed onto the stack is the first one to be executed.

- -

According to the ECMAScript specification, the call stack is defined as part of the execution context. Whenever a function is called, a new execution context is created and placed at the top of the stack. Once the function completes, its execution context is removed from the stack, and control returns to the previous context. This helps manage synchronous code execution, as each function call must complete before the next one can begin.

- -### Reference - -- [Call Stack — MDN](https://developer.mozilla.org/en-US/docs/Glossary/Call_stack) - -### Articles - -- [Understanding Javascript Call Stack, Event Loops — Gaurav Pandvia](https://medium.com/@gaurav.pandvia/understanding-javascript-function-executions-tasks-event-loop-call-stack-more-part-1-5683dea1f5ec) -- [Understanding the JavaScript Call Stack — Charles Freeborn](https://medium.freecodecamp.org/understanding-the-javascript-call-stack-861e41ae61d4) -- [Javascript: What Is The Execution Context? What Is The Call Stack? — Valentino Gagliardi](https://medium.com/@valentinog/javascript-what-is-the-execution-context-what-is-the-call-stack-bd23c78f10d1) -- [What is the JS Event Loop and Call Stack? — Jess Telford](https://gist.github.com/jesstelford/9a35d20a2aa044df8bf241e00d7bc2d0) -- [Understanding Execution Context and Execution Stack in Javascript — Sukhjinder Arora](https://blog.bitsrc.io/understanding-execution-context-and-execution-stack-in-javascript-1c9ea8642dd0) -- [How JavaScript Works Under The Hood: An Overview of JavaScript Engine, Heap and, Call Stack — Bipin Rajbhar](https://dev.to/bipinrajbhar/how-javascript-works-under-the-hood-an-overview-of-javascript-engine-heap-and-call-stack-1j5o) -- [The JS Call stack Explained in 9 minutes](https://www.youtube.com/watch?v=W8AeMrVtFLY) - Colt Steel (YouTube) - -### video Videos - -- [Javascript: the Call Stack explained — Coding Blocks India](https://www.youtube.com/watch?v=w6QGEiQceOM) -- [The JS Call Stack Explained In 9 Minutes — Colt Steele](https://www.youtube.com/watch?v=W8AeMrVtFLY) -- [What is the Call Stack? — Eric Traub](https://www.youtube.com/watch?v=w7QWQlkLY_s) -- [The Call Stack — Kevin Drumm](https://www.youtube.com/watch?v=Q2sFmqvpBe0) -- [Understanding JavaScript Execution — Codesmith](https://www.youtube.com/watch?v=Z6a1cLyq7Ac&list=PLWrQZnG8l0E4kd1T_nyuVoxQUaYEWFgcD) -- [What the heck is the event loop anyway? — Philip Roberts](https://www.youtube.com/watch?v=8aGhZQkoFbQ) -- [How JavaScript Code is executed? ❤️& Call Stack — Akshay Saini](https://www.youtube.com/watch?v=iLWTnMzWtj4&list=PLlasXeu85E9cQ32gLCvAvr9vNaUccPVNP) -- [Call Stacks - CS50](https://www.youtube.com/watch?v=aCPkszeKRa4) -- [Learn the JavaScript Call Stack - codecupdev](https://www.youtube.com/watch?v=HXqXPGS96rw) -- [JavaScript Functions and the Call Stack | How does the Call stack work - Chidre'sTechTutorials](https://www.youtube.com/watch?v=P6H-T4cUDR4) - -**[⬆ Back to Top](#table-of-contents)** - ---- - -## 2. Primitive Types - -

According to the ECMAScript specification, JavaScript has six primitive data types: string, number, bigint, boolean, undefined, and symbol. These types are immutable, meaning their values cannot be altered. There is also a special primitive type called null, which represents the intentional absence of any object value.

- -

Primitive values are directly assigned to a variable, and when you manipulate a primitive type, you're working directly on the value. Unlike objects, primitives do not have properties or methods, but JavaScript automatically wraps primitive values with object counterparts when necessary (e.g., when calling methods on strings).

- -### Reference - -- [JavaScript data types and data structures — MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Primitive_values) - -### Articles - -- [Primitive and Non-primitive data-types in JavaScript - GeeksforGeeks](https://www.geeksforgeeks.org/primitive-and-non-primitive-data-types-in-javascript) -- [How numbers are encoded in JavaScript — Dr. Axel Rauschmayer](http://2ality.com/2012/04/number-encoding.html) -- [What Every JavaScript Developer Should Know About Floating Point Numbers — Chewxy](https://blog.chewxy.com/2014/02/24/what-every-javascript-developer-should-know-about-floating-point-numbers/) -- [The Secret Life of JavaScript Primitives — Angus Croll](https://javascriptweblog.wordpress.com/2010/09/27/the-secret-life-of-javascript-primitives/) -- [Primitive Types — Flow](https://flow.org/en/docs/types/primitives/) -- [(Not) Everything in JavaScript is an Object — Daniel Li](https://dev.to/d4nyll/not-everything-in-javascript-is-an-object) -- [JavaScript data types and data structures — MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Primitive_values) -- [Diving Deeper in JavaScripts Objects — Arfat Salman](https://blog.bitsrc.io/diving-deeper-in-javascripts-objects-318b1e13dc12) -- [The differences between Object.freeze() vs Const in JavaScript — Bolaji Ayodeji](https://medium.com/@bolajiayodeji/the-differences-between-object-freeze-vs-const-in-javascript-4eacea534d7c) -- [Object to primitive conversion — JavaScript.Info](https://javascript.info/object-toprimitive) -- [Methods of primitives - Javascript.info](https://javascript.info/primitives-methods) - -### video Videos - -- [JavaScript Reference vs Primitive Types — Academind](https://www.youtube.com/watch?v=9ooYYRLdg_g) -- [JavaScript Primitive Types — Simon Sez IT](https://www.youtube.com/watch?v=HsbWQsSCE5Y) -- [Value Types and Reference Types in JavaScript — Programming with Mosh](https://www.youtube.com/watch?v=e-_mDyqm2oU) -- [JavaScript Primitive Data Types — Avelx](https://www.youtube.com/watch?v=qw3j0A3DIzQ) -- [Everything you never wanted to know about JavaScript numbers — Bartek Szopka](https://www.youtube.com/watch?v=MqHDDtVYJRI) -- [What are variables in Javascript? — JS For Everyone](https://www.youtube.com/watch?v=B4Bbmei_thw) -- [TIPOS DE DATOS PRIMITIVOS en JAVASCRIPT - La Cocina del Código](https://www.youtube.com/watch?v=cC65D2q5f8I) -- [Data Type in JavaScript - ScholarHat](https://www.youtube.com/watch?v=aFDvBjVjCh8) - -**[⬆ Back to Top](#table-of-contents)** - ---- - -## 3. Value Types and Reference Types - -

According to the ECMAScript specification, value types are stored directly in the location that the variable accesses. These include types like number, string, boolean, undefined, bigint, symbol, and null. When you assign a value type to a variable, the value itself is stored.

- -

Reference types, on the other hand, are objects stored in the heap. Variables assigned to reference types actually store references (pointers) to the objects, not the objects themselves. When you assign a reference type to another variable, both variables reference the same object in memory.

- -### Articles - -- [Explaining Value vs. Reference in Javascript — Arnav Aggarwal](https://codeburst.io/explaining-value-vs-reference-in-javascript-647a975e12a0) -- [Primitive Types & Reference Types in JavaScript — Bran van der Meer](https://gist.github.com/branneman/7fb06d8a74d7e6d4cbcf75c50fec599c) -- [Value Types, Reference Types and Scope in JavaScript — Ben Aston](https://medium.com/@benastontweet/lesson-1b-javascript-fundamentals-380f601ba851) -- [Back to roots: JavaScript Value vs Reference — Miro Koczka](https://medium.com/dailyjs/back-to-roots-javascript-value-vs-reference-8fb69d587a18) -- [Grasp "By Value" and "By Reference" in JavaScript — Léna Faure](https://hackernoon.com/grasp-by-value-and-by-reference-in-javascript-7ed75efa1293) -- [JavaScript Reference and Copy Variables — Vítor Capretz](https://hackernoon.com/javascript-reference-and-copy-variables-b0103074fdf0) -- [JavaScript Primitive vs Reference Values](http://www.javascripttutorial.net/javascript-primitive-vs-reference-values/) -- [JavaScript by Reference vs. by Value — nrabinowitz](https://stackoverflow.com/questions/6605640/javascript-by-reference-vs-by-value) -- [JavaScript Interview Prep: Primitive vs. Reference Types — Mike Cronin](https://dev.to/mostlyfocusedmike/javascript-interview-prep-primitive-vs-reference-types-3o4f) -- [JavaScript map vs. forEach: When to Use Each One - Sajal Soni](https://code.tutsplus.com/tutorials/javascript-map-vs-foreach-when-to-use-each-one--cms-38365) - -### video Videos - -- [Javascript Pass by Value vs Pass by Reference — techsith](https://www.youtube.com/watch?v=E-dAnFdq8k8) -- [JavaScript Value vs Reference Types — Programming with Mosh](https://www.youtube.com/watch?v=fD0t_DKREbE) -- [VALORES vs REFERENCIAS en JAVASCRIPT - La Cocina del Código](https://www.youtube.com/watch?v=AvkyOrWkuQc) -- [JavaScript - Reference vs Primitive Values/ Types - Academind](https://www.youtube.com/watch?v=9ooYYRLdg_g) -- [Value Types and Reference Types in JavaScript - Programming with Mosh](https://www.youtube.com/watch?v=e-_mDyqm2oU) - -**[⬆ Back to Top](#table-of-contents)** - ---- - -## 4. Implicit, Explicit, Nominal, Structuring and Duck Typing - -

The ECMAScript specification defines JavaScript as a dynamically typed language, meaning that types are associated with values rather than variables, and type checking occurs at runtime. There are various ways JavaScript manages types:

- -

Implicit Typing (or Type Coercion): This occurs when JavaScript automatically converts one data type to another when required. For instance, JavaScript might convert a string to a number during an arithmetic operation. While this can simplify some code, it can also lead to unexpected results if not handled carefully.

- -

Explicit Typing: Unlike implicit typing, explicit typing involves manually converting a value from one type to another using functions like Number(), String(), or Boolean().

- -

Nominal Typing: JavaScript doesn't natively support nominal typing, where types are explicitly declared and checked. However, TypeScript, a superset of JavaScript, brings this feature to help catch type errors during development.

- -

Structural Typing: In this type system, types are based on the structure or properties of the data. JavaScript is a structurally typed language where objects are compatible if they share the same structure (i.e., the same set of properties and methods).

- -

Duck Typing: This is a concept where an object's suitability is determined by the presence of certain properties and methods, rather than by the actual type of the object. JavaScript relies heavily on duck typing, where behavior is inferred from an object's properties rather than its declared type.

- -### Articles - -- [What you need to know about Javascript's Implicit Coercion — Promise Tochi](https://dev.to/promhize/what-you-need-to-know-about-javascripts-implicit-coercion-e23) -- [JavaScript Type Coercion Explained — Alexey Samoshkin](https://medium.freecodecamp.org/js-type-coercion-explained-27ba3d9a2839) -- [Javascript Coercion Explained — Ben Garrison](https://hackernoon.com/javascript-coercion-explained-545c895213d3) -- [What exactly is Type Coercion in Javascript? - Stack Overflow](https://stackoverflow.com/questions/19915688/what-exactly-is-type-coercion-in-javascript) - -### video Videos - -- [== ? === ??? ...#@^% - Shirmung Bielefeld](https://www.youtube.com/watch?v=qGyqzN0bjhc&t) -- [Coercion in Javascript - Hitesh Choudhary](https://www.youtube.com/watch?v=b04Q_vyqEG8) -- [JavaScript Questions: What is Coercion? - Steven Hancock](https://www.youtube.com/watch?v=z4-8wMSPJyI) -- [Typing: Static vs Dynamic, Weak vs. Strong - Codexpanse](https://www.youtube.com/watch?v=C5fr0LZLMAs) -- [EL SISTEMA de TIPOS DE JAVASCRIPT - La Cocina del Código](https://www.youtube.com/watch?v=0ei4nb49GKo) -- [Duck Typing in Javascript - Techmaker Studio](https://www.youtube.com/watch?v=oEpgyoMEkrM) -- [Duck Typing in Javascript - Programming with Kartik](https://youtu.be/e4X1KAuk6Bs?si=krZKbsM2i3tmIl2G) - -### Books - -- [You Don't Know JS, 1st Edition: Types & Grammar — Kyle Simpson](https://github.com/getify/You-Dont-Know-JS/tree/1st-ed) - -**[⬆ Back to Top](#table-of-contents)** - ---- - -## 5. == vs === vs typeof - According to the ECMAScript specification, JavaScript includes both strict (===) and loose (==) equality operators, which behave differently when comparing values. Here's a breakdown: - -== (Loose Equality): This operator performs type coercion before comparing two values. If the values are of different types, JavaScript will attempt to convert one or both values to a common type before comparison, which can lead to unexpected results. - -=== (Strict Equality): This operator compares both the value and the type without any type coercion. If the two values are not of the same type, the comparison will return false. - -typeof Operator: The typeof operator is used to check the data type of a variable. While it's generally reliable, there are certain quirks, like how typeof null returns "object" instead of "null", due to a long-standing behavior in JavaScript's implementation. - -### Articles - -- [JavaScript Double Equals vs. Triple Equals — Brandon Morelli](https://codeburst.io/javascript-double-equals-vs-triple-equals-61d4ce5a121a) -- [Should I use === or == equality comparison operator in JavaScript? — Panu Pitkamaki](https://bytearcher.com/articles/equality-comparison-operator-javascript/) -- [Why Use the Triple-Equals Operator in JavaScript? — Louis Lazaris](https://www.impressivewebs.com/why-use-triple-equals-javascipt/) -- [What is the difference between == and === in JavaScript? — Craig Buckler](https://www.oreilly.com/learning/what-is-the-difference-between-and-in-javascript) -- [Why javascript's typeof always return "object"? — Stack Overflow](https://stackoverflow.com/questions/3787901/why-javascripts-typeof-always-return-object) -- [Checking Types in Javascript — Toby Ho](http://tobyho.com/2011/01/28/checking-types-in-javascript/) -- [How to better check data types in JavaScript — Webbjocke](https://webbjocke.com/javascript-check-data-types/) -- [Checking for the Absence of a Value in JavaScript — Tomer Aberbach](https://tomeraberba.ch/html/post/checking-for-the-absence-of-a-value-in-javascript.html) -- [Difference Between == and === in Javascript](https://www.scaler.com/topics/javascript/difference-between-double-equals-and-triple-equals-in-javascript/) -- [Difference between == and === in JavaScript — GeeksforGeeks](https://www.geeksforgeeks.org/difference-between-double-equal-vs-triple-equal-javascript/) -- [=== vs == Comparision in JavaScript — FreeCodeCamp](https://www.freecodecamp.org/news/javascript-triple-equals-sign-vs-double-equals-sign-comparison-operators-explained-with-examples/) - -### video Videos - -- [JavaScript - The typeof operator — Java Brains](https://www.youtube.com/watch?v=ol_su88I3kw) -- [Javascript typeof operator — DevDelight](https://www.youtube.com/watch?v=qPYhTPt_SbQ) -- [JavaScript "==" VS "===" — Web Dev Simplified](https://www.youtube.com/watch?v=C5ZVC4HHgIg) -- [=== vs == in javascript - Hitesh Choudhary](https://www.youtube.com/watch?v=a0S1iG3TgP0) -- [The typeof operator in JS - CodeVault](https://www.youtube.com/watch?v=NSS5WRcv7yM) - -**[⬆ Back to Top](#table-of-contents)** - ---- - -## 6. Function Scope, Block Scope and Lexical Scope - The ECMAScript specification outlines three key types of scope: - -Function Scope: Variables declared within a function using var are only accessible within that function. This scope isolates variables from being accessed outside of the function where they are declared. - -Block Scope: Introduced with ES6, variables declared with let and const are block-scoped. This means they are only accessible within the specific block {} in which they are defined, such as inside loops or conditionals. - -Lexical Scope: Refers to how variable access is determined based on the physical location of the variables in the code. Functions are lexically scoped, meaning that they can access variables from their parent scope. - -### Books - -- [You Don't Know JS Yet, 2nd Edition: Scope & Closures — Kyle Simpson](https://github.com/getify/You-Dont-Know-JS/tree/2nd-ed/scope-closures) - -### Articles - -- [JavaScript Functions — Understanding The Basics — Brandon Morelli](https://codeburst.io/javascript-functions-understanding-the-basics-207dbf42ed99) -- [Var, Let, and Const – What's the Difference?](https://www.freecodecamp.org/news/var-let-and-const-whats-the-difference/) -- [Functions in JavaScript - Deepa Pandey](https://www.scaler.com/topics/javascript/javascript-functions/) -- [Emulating Block Scope in JavaScript — Josh Clanton](http://adripofjavascript.com/blog/drips/emulating-block-scope-in-javascript.html) -- [The Difference Between Function and Block Scope in JavaScript — Joseph Cardillo](https://medium.com/@josephcardillo/the-difference-between-function-and-block-scope-in-javascript-4296b2322abe) -- [Understanding Scope and Context in JavaScript — Ryan Morr](http://ryanmorr.com/understanding-scope-and-context-in-javascript/) -- [JavaScript Scope and Closures — Zell Liew](https://css-tricks.com/javascript-scope-closures/) -- [Understanding Scope in JavaScript — Wissam Abirached](https://developer.telerik.com/topics/web-development/understanding-scope-in-javascript/) -- [Understanding Scope in JavaScript ― Hammad Ahmed](https://scotch.io/tutorials/understanding-scope-in-javascript) -- [When to use a function declaration vs. a function expression ― Amber Wilkie](https://medium.freecodecamp.org/when-to-use-a-function-declarations-vs-a-function-expression-70f15152a0a0) -- [A JavaScript Fundamentals Cheat Sheet: Scope, Context, and "this" ― Alexandra Fren](https://dev.to/alexandrafren/a-javascript-fundamentals-cheat-sheet-scope-context-and-this-28ai) -- [Functions / Function scope ― MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Functions#function_scope) - -### video Videos - -- [What Makes Javascript Weird ... and Awesome pt. 4 — LearnCode.academy](https://www.youtube.com/watch?v=SBwoFkRjZvE) -- [Variable Scope in JavaScript — Kirupa Chinnathambi](https://www.youtube.com/watch?v=dhp57T3p760) -- [JavaScript Block Scope and Function Scope — mmtuts](https://www.youtube.com/watch?v=aK_nuUAdr8E) -- [What the Heck is Lexical Scope? — NWCalvank](https://www.youtube.com/watch?v=GhNA0r10MmA) -- [Variable Scope — Steve Griffith](https://www.youtube.com/watch?v=FyWdrCZZavQ) -- [Javascript Tutorials for Beginners — Mosh Hemadani](https://www.youtube.com/watch?v=W6NZfCO5SIk) -- [JavaScript Block scope vs Function scope - nivek](https://www.youtube.com/watch?v=IaTztAtoNEY) -- [Lexical scoping in javascript - Hitesh Choudhary](https://www.youtube.com/watch?v=qT5S7GgIioE) -- [Modern Scope Handling in JavaScript (ES6 and Beyond) -Prashant Dewangan ](https://www.youtube.com/watch?v=zMseUdOR7z8) - -**[⬆ Back to Top](#table-of-contents)** - ---- - -## 7. Expression vs Statement -According to the ECMAScript specification, expressions produce a value, and statements are instructions to perform an action, such as variable assignment or control flow. Function declarations are hoisted and can be called before they are defined in the code, while function expressions are not hoisted and must be defined before being invoked. - -### Articles - -- [All you need to know about Javascript's Expressions, Statements and Expression Statements — Promise Tochi](https://dev.to/promhize/javascript-in-depth-all-you-need-to-know-about-expressions-statements-and-expression-statements-5k2) -- [Function Expressions vs Function Declarations — Paul Wilkins](https://www.sitepoint.com/function-expressions-vs-declarations/) -- [JavaScript Function — Declaration vs Expression — Ravi Roshan](https://medium.com/@raviroshan.talk/javascript-function-declaration-vs-expression-f5873b8c7b38) -- [Function Declarations vs. Function Expressions — Mandeep Singh](https://medium.com/@mandeep1012/function-declarations-vs-function-expressions-b43646042052) -- [Function Declarations vs. Function Expressions — Anguls Croll](https://javascriptweblog.wordpress.com/2010/07/06/function-declarations-vs-function-expressions/) -- [Expression statement — MDN web docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/Expression_statement) - - -### video Videos - -- [Expressions vs. Statements in JavaScript — Hexlet](https://www.youtube.com/watch?v=WVyCrI1cHi8) -- [JavaScript - Expression vs. Statement — WebTunings](https://www.youtube.com/watch?v=3jDpNGJkupA) -- [Javascript Function Expression Vs Declaration For Beginners — Dev Material](https://www.youtube.com/watch?v=qz7Nq1tV7Io) -- [The difference between an expression and a statement in JavaScript](https://youtu.be/eWTuFoBYiwg) -- [Expression in javascript | Statement in javascript - Sathelli Srikanth](https://www.youtube.com/watch?v=cVDs3TZ-kXs) - - -**[⬆ Back to Top](#table-of-contents)** - ---- - -## 8. IIFE, Modules and Namespaces -With the introduction of ES6 modules, the role of IIFEs in scope isolation has diminished but they still remain relevant. -### Reference - -- [IIFE — MDN](https://developer.mozilla.org/en-US/docs/Glossary/IIFE) -- [Modularity — MDN](https://developer.mozilla.org/en-US/docs/Glossary/modularity) -- [Namespace — MDN](https://developer.mozilla.org/en-US/docs/Glossary/Namespace) - -### Articles - -- [Mastering Immediately-Invoked Function Expressions ― Chandra Gundamaraju](https://medium.com/@vvkchandra/essential-javascript-mastering-immediately-invoked-function-expressions-67791338ddc6) -- [JavaScript Immediately Invoked Function Expression — javascripttutorial.net](https://www.javascripttutorial.net/javascript-immediately-invoked-function-expression-iife/) -- [A 10 minute primer to JavaScript modules, module formats, module loaders and module bundlers ― Jurgen Van de Moere](https://www.jvandemo.com/a-10-minute-primer-to-javascript-modules-module-formats-module-loaders-and-module-bundlers/) -- [Modules ― Exploring JS](http://exploringjs.com/es6/ch_modules.html) -- [Understanding ES6 Modules — Craig Buckler](https://www.sitepoint.com/understanding-es6-modules/) -- [An overview of ES6 Modules in JavaScript — Brent Graham](https://blog.cloud66.com/an-overview-of-es6-modules-in-javascript/) -- [ES6 Modules in Depth — Nicolás Bevacqua](https://ponyfoo.com/articles/es6-modules-in-depth) -- [ES6 modules, Node.js and the Michael Jackson Solution — Alberto Gimeno](https://medium.com/dailyjs/es6-modules-node-js-and-the-michael-jackson-solution-828dc244b8b) -- [JavaScript Modules: A Beginner's Guide — Preethi Kasireddy](https://medium.freecodecamp.org/javascript-modules-a-beginner-s-guide-783f7d7a5fcc) -- [Using JavaScript modules on the web — Addy Osmani & Mathias Bynens](https://developers.google.com/web/fundamentals/primers/modules) -- [IIFE: Immediately Invoked Function Expressions — Parwinder](https://dev.to/bhagatparwinder/iife-immediately-invoked-function-expressions-49c5) -- [Javascript Module Bundlers — Vanshu Hassija](https://sassy-butter-197.notion.site/Javascript-bundlers-016932b17b0744e983c2cc0db31e6f02) - -### video Videos - -- [Immediately Invoked Function Expression - Beau teaches JavaScript — freeCodeCamp](https://www.youtube.com/watch?v=3cbiZV4H22c) -- [Understanding JavaScript IIFE — Sheo Narayan](https://www.youtube.com/watch?v=I5EntfMeIIQ) -- [JavaScript Modules: ES6 Import and Export — Kyle Robinson](https://www.youtube.com/watch?v=_3oSWwapPKQ) -- [ES6 - Modules — Ryan Christiani](https://www.youtube.com/watch?v=aQr2bV1BPyE) -- [ES6 Modules in the Real World — Sam Thorogood](https://www.youtube.com/watch?v=fIP4pjAqCtQ) -- [ES6 Modules — TempleCoding](https://www.youtube.com/watch?v=5P04OK6KlXA) -- [JavaScript IIFE (Immediately Invoked Function Expressions) — Steve Griffith](https://www.youtube.com/watch?v=Xd7zgPFwVX8&) - -**[⬆ Back to Top](#table-of-contents)** - --- -## 9. Message Queue and Event Loop -The Event Loop is a critical part of JavaScript's concurrency model, ensuring non-blocking behavior by processing tasks in an asynchronous manner. Understanding how it interacts with the Message Queue and Microtasks is key to mastering JavaScript behavior. -### Articles +## About -- [JavaScript Event Loop Explained — Anoop Raveendran](https://medium.com/front-end-hacking/javascript-event-loop-explained-4cd26af121d4) -- [Understanding JS: The Event Loop — Alexander Kondov](https://hackernoon.com/understanding-js-the-event-loop-959beae3ac40) -- [The JavaScript Event Loop — Flavio Copes](https://flaviocopes.com/javascript-event-loop/) -- [Tasks, microtasks, queues and schedules — Jake Archibald](https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/) -- [Visualising the JavaScript Event Loop with a Pizza Restaurant analogy — Priyansh Jain](https://dev.to/presto412/visualising-the-javascript-event-loop-with-a-pizza-restaurant-analogy-47a8) -- [JavaScript Visualized: Event Loop — Lydia Hallie](https://dev.to/lydiahallie/javascript-visualized-event-loop-3dif) -- [Understanding and Optimizing JavaScript's Event Loop — Xiuer Old](https://medium.com/javascript-zone/understanding-and-optimizing-javascripts-event-loop-717ae0095038#:~:text=The%20event%20loop%20is%20the,%2Dblocking%20I%2FO%20operations.) +This repository helps developers master core JavaScript concepts. Each concept includes clear explanations, practical code examples, and curated resources. -### video Videos - -- [What the heck is the event loop anyway? | JSConf EU — Philip Roberts](https://www.youtube.com/watch?v=8aGhZQkoFbQ) -- [JavaScript Event Loop — ComScience Simplified](https://www.youtube.com/watch?v=XzXIMZMN9k4) -- [I'm stuck in an Event Loop — Philip Roberts](https://www.youtube.com/watch?v=6MXRNXXgP_0) -- [In The Loop - Jake Archibald | JSConf.Asia 2018](https://www.youtube.com/watch?v=cCOL7MC4Pl0) -- [Desmitificando el Event Loop (Spanish)](https://www.youtube.com/watch?v=Eqq2Rb7LzYE) -- [Callbacks, Sincrono, Assíncrono e Event Loop (PT-BR)](https://www.youtube.com/watch?v=6lbBaM18X3g) -- [JavaScript Event Loop: How it Works and Why it Matters in 5 Minutes - James Q Quick](https://www.youtube.com/watch?v=6lbBaM18X3g) - -**[⬆ Back to Top](#table-of-contents)** +**[Start learning at 33jsconcepts.com →](https://33jsconcepts.com)** --- -## 10. setTimeout, setInterval and requestAnimationFrame +## Concepts -### Articles +### Fundamentals -- [setTimeout and setInterval — JavaScript.Info](https://javascript.info/settimeout-setinterval) -- [Why not to use setInterval — Akanksha Sharma](https://dev.to/akanksha_9560/why-not-to-use-setinterval--2na9) -- [setTimeout VS setInterval — Develoger](https://develoger.com/settimeout-vs-setinterval-cff85142555b) -- [Using requestAnimationFrame — Chris Coyier](https://css-tricks.com/using-requestanimationframe/) -- [Understanding JavaScript's requestAnimationFrame() — JavaScript Kit](http://www.javascriptkit.com/javatutors/requestanimationframe.shtml) -- [Handling time intervals in JavaScript - Amit Merchant](https://www.amitmerchant.com/Handling-Time-Intervals-In-Javascript/) -- [Debounce – How to Delay a Function in JavaScript - Ondrej Polesny](https://www.freecodecamp.org/news/javascript-debounce-example/) +- **[Primitive Types](https://33jsconcepts.com/concepts/primitive-types)** + Learn JavaScript's 7 primitive types: string, number, bigint, boolean, undefined, null, and symbol. Understand immutability, typeof quirks, and autoboxing. -### video Videos +- **[Value vs Reference Types](https://33jsconcepts.com/concepts/value-reference-types)** + Learn how value types and reference types work in JavaScript. Understand how primitives and objects are stored, why copying objects shares references, and how to avoid mutation bugs. -- [Javascript: How setTimeout and setInterval works — Coding Blocks India](https://www.youtube.com/watch?v=6bPKyl8WYWI) -- [TRUST ISSUES with setTimeout() — Akshay Saini ](https://youtu.be/nqsPmuicJJc?si=4FXKlZfqiJUqO2Y4) -- [setTimeout and setInterval in JavaScript — techsith](https://www.youtube.com/watch?v=TbCgGWe8LN8) -- [JavaScript Timers — Steve Griffith](https://www.youtube.com/watch?v=0VVJSvlUgtg) -- [JavaScript setTimeOut and setInterval Explained — Theodore Anderson](https://www.youtube.com/watch?v=mVKfrWCOB60) +- **[Type Coercion](https://33jsconcepts.com/concepts/type-coercion)** + Learn JavaScript type coercion and implicit conversion. Understand how values convert to strings, numbers, and booleans, the 8 falsy values, and how to avoid common coercion bugs. -**[⬆ Back to Top](#table-of-contents)** +- **[Equality Operators](https://33jsconcepts.com/concepts/equality-operators)** + Learn JavaScript equality operators == vs ===, typeof quirks, and Object.is(). Understand type coercion, why NaN !== NaN, and why typeof null returns 'object'. ---- +- **[Scope and Closures](https://33jsconcepts.com/concepts/scope-and-closures)** + Learn JavaScript scope and closures. Understand the three types of scope, var vs let vs const, lexical scoping, the scope chain, and closure patterns for data privacy. -## 11. JavaScript Engines +- **[Call Stack](https://33jsconcepts.com/concepts/call-stack)** + Learn how the JavaScript call stack tracks function execution. Understand stack frames, LIFO ordering, execution contexts, stack overflow errors, and debugging with stack traces. +### Functions & Execution -### Articles +- **[Event Loop](https://33jsconcepts.com/concepts/event-loop)** + Learn how the JavaScript event loop handles async code. Understand the call stack, task queue, microtasks, and why Promises always run before setTimeout(). -- [JavaScript Engines — Jen Looper](http://www.softwaremag.com/javascript-engines/) -- [Understanding How the Chrome V8 Engine Translates JavaScript into Machine Code — DroidHead](https://medium.freecodecamp.org/understanding-the-core-of-nodejs-the-powerful-chrome-v8-engine-79e7eb8af964) -- [Understanding V8's Bytecode — Franziska Hinkelmann](https://medium.com/dailyjs/understanding-v8s-bytecode-317d46c94775) -- [JavaScript essentials: why you should know how the engine works - Rainer Hahnekamp](https://www.freecodecamp.org/news/javascript-essentials-why-you-should-know-how-the-engine-works-c2cc0d321553) -- [JavaScript engine fundamentals: Shapes and Inline Caches](https://mathiasbynens.be/notes/shapes-ics) -- [JavaScript engine fundamentals: optimizing prototypes](https://mathiasbynens.be/notes/prototypes) -- [How V8 optimizes array operations](https://v8.dev/blog/elements-kinds) -- [JavaScript Internals: JavaScript engine, Run-time environment & setTimeout Web API — Rupesh Mishra](https://blog.bitsrc.io/javascript-internals-javascript-engine-run-time-environment-settimeout-web-api-eeed263b1617) +- **[IIFE, Modules & Namespaces](https://33jsconcepts.com/concepts/iife-modules)** + Learn how to organize JavaScript code with IIFEs, namespaces, and ES6 modules. Understand private scope, exports, dynamic imports, and common module mistakes. -### video Videos +### Web Platform -- [JavaScript Engines: The Good Parts™ — Mathias Bynens & Benedikt Meurer](https://www.youtube.com/watch?v=5nmpokoRaZI) -- [JS Engine EXPOSED 🔥 Google's V8 Architecture 🚀 | Namaste JavaScript Ep. 16 - Akshay Saini](https://www.youtube.com/watch?v=2WJL19wDH68) -- [How JavaScript Code is executed? How Javascript works behind the scenes](https://youtu.be/iLWTnMzWtj4) -- [Understanding the V8 JavaScript Engine - freeCodeCamp Talks](https://www.youtube.com/watch?v=xckH5s3UuX4) -- [JavaScript Under The Hood - JavaScript Engine Overview - Traversy Media](https://www.youtube.com/watch?v=oc6faXVc54E) -- [Arindam Paul - JavaScript VM internals, EventLoop, Async and ScopeChains](https://www.youtube.com/watch?v=QyUFheng6J0) +- **[DOM](https://33jsconcepts.com/concepts/dom)** + Learn how the DOM works in JavaScript. Understand how browsers represent HTML as a tree, select and manipulate elements, traverse nodes, and optimize rendering performance. -**[⬆ Back to Top](#table-of-contents)** +- **[Fetch API](https://33jsconcepts.com/concepts/http-fetch)** + Learn how to make HTTP requests with the JavaScript Fetch API. Understand GET, POST, response handling, JSON parsing, error patterns, and AbortController for cancellation. ---- +- **[Web Workers](https://33jsconcepts.com/concepts/web-workers)** + Learn Web Workers in JavaScript for running code in background threads. Understand postMessage, Dedicated and Shared Workers, and transferable objects. -## 12. Bitwise Operators, Type Arrays and Array Buffers +### Object-Oriented JavaScript -### Articles +- **[Factories and Classes](https://33jsconcepts.com/concepts/factories-classes)** + Learn JavaScript factory functions and ES6 classes. Understand constructors, prototypes, private fields, inheritance, and when to use each pattern. -- [Programming with JS: Bitwise Operations — Alexander Kondov](https://hackernoon.com/programming-with-js-bitwise-operations-393eb0745dc4) -- [Using JavaScript's Bitwise Operators in Real Life — ian m](https://codeburst.io/using-javascript-bitwise-operators-in-real-life-f551a731ff5) -- [JavaScript Bitwise Operators — w3resource](https://www.w3resource.com/javascript/operators/bitwise-operator.php) -- [Bitwise Operators in Javascript — Joe Cha](https://medium.com/bother7-blog/bitwise-operators-in-javascript-65c4c69be0d3) -- [A Comprehensive Primer on Binary Computation and Bitwise Operators in javascript — Paul Brown](https://medium.com/techtrument/a-comprehensive-primer-on-binary-computation-and-bitwise-operators-in-javascript-81acf8341f04) -- [How can I understand Bitwise operation in JavaScript?](https://www.quora.com/How-can-I-understand-Bitwise-operation-in-JavaScript) +- **[this, call, apply, bind](https://33jsconcepts.com/concepts/this-call-apply-bind)** + Learn how JavaScript's 'this' keyword works and how to control context binding. Understand the 5 binding rules, call/apply/bind methods, arrow functions, and common pitfalls. -### video Videos +- **[Object Creation & Prototypes](https://33jsconcepts.com/concepts/object-creation-prototypes)** + Learn JavaScript's prototype chain and object creation. Understand how inheritance works, the new operator's 4 steps, Object.create(), Object.assign(), and prototype methods. -- [JavaScript Bitwise Operators — Programming with Mosh](https://www.youtube.com/watch?v=mesu75PTDC8) -- [Bitwise Operators and WHY we use them — Alex Hyett](https://www.youtube.com/watch?v=igIjGxF2J-w) -- [JS Bitwise Operators and Binary Numbers — Steve Griffith - Prof3ssorSt3v3](https://www.youtube.com/watch?v=RRyxCmLX_ag) -- [Deep Dive into Blobs, Files, and ArrayBuffers — Steve Griffith - Prof3ssorSt3v3](https://www.youtube.com/watch?v=ScZZoHj7mqY) +- **[Inheritance & Polymorphism](https://33jsconcepts.com/concepts/inheritance-polymorphism)** + Learn inheritance and polymorphism in JavaScript — extending classes, prototype chains, method overriding, and code reuse patterns. -**[⬆ Back to Top](#table-of-contents)** +### Async JavaScript ---- +- **[Callbacks](https://33jsconcepts.com/concepts/callbacks)** + Learn JavaScript callbacks, functions passed to other functions to be called later. Understand sync vs async callbacks, error-first patterns, callback hell, and why Promises were invented. -## 13. DOM and Layout Trees +- **[Promises](https://33jsconcepts.com/concepts/promises)** + Learn JavaScript Promises for handling async operations. Understand how to create, chain, and combine Promises, handle errors properly, and avoid common pitfalls. -### Reference +- **[async/await](https://33jsconcepts.com/concepts/async-await)** + Learn async/await in JavaScript. Syntactic sugar over Promises that makes async code readable. Covers error handling with try/catch, parallel execution with Promise.all, and common pitfalls. -- [Document Object Model (DOM) — MDN](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model) +- **[Generators & Iterators](https://33jsconcepts.com/concepts/generators-iterators)** + Learn JavaScript generators and iterators. Understand yield, the iteration protocol, lazy evaluation, infinite sequences, and async generators with for await...of. -### Books +### Functional Programming -- [Eloquent JavaScript, 3rd Edition: Ch. 14 - The Document Object Model](https://eloquentjavascript.net/14_dom.html) +- **[Higher-Order Functions](https://33jsconcepts.com/concepts/higher-order-functions)** + Learn higher-order functions in JavaScript. Understand functions that accept or return other functions, create reusable abstractions, and write cleaner code. -### Articles +- **[Pure Functions](https://33jsconcepts.com/concepts/pure-functions)** + Learn pure functions in JavaScript. Understand the two rules of purity, avoid side effects, and write testable, predictable code with immutable patterns. -- [How To Understand and Modify the DOM in JavaScript — Tania Rascia](https://www.digitalocean.com/community/tutorials/introduction-to-the-dom) -- [What's the Document Object Model, and why you should know how to use it — Leonardo Maldonado](https://medium.freecodecamp.org/whats-the-document-object-model-and-why-you-should-know-how-to-use-it-1a2d0bc5429d) -- [JavaScript DOM Tutorial with Example — Guru99](https://www.guru99.com/how-to-use-dom-and-events-in-javascript.html) -- [What is the DOM? — Chris Coyier](https://css-tricks.com/dom/) -- [Traversing the DOM with JavaScript — Zell Liew](https://zellwk.com/blog/dom-traversals/) -- [DOM Tree](https://javascript.info/dom-nodes) -- [How to traverse the DOM in Javascript — Vojislav Grujić](https://medium.com/javascript-in-plain-english/how-to-traverse-the-dom-in-javascript-d6555c335b4e) -- [Render Tree Construction — Ilya Grigorik](https://developers.google.com/web/fundamentals/performance/critical-rendering-path/render-tree-construction) -- [What exactly is the DOM?](https://bitsofco.de/what-exactly-is-the-dom/) -- [JavaScript DOM](https://www.javascripttutorial.net/javascript-dom/) -- [Traversing the Dom with Javascript](https://www.youtube.com/watch?v=Pr4LLrmDLLo) - Steve Griffith (YouTube) +- **[map, reduce, filter](https://33jsconcepts.com/concepts/map-reduce-filter)** + Learn map, reduce, and filter in JavaScript. Transform, filter, and combine arrays without mutation. Includes method chaining and common pitfalls. -### video Videos +- **[Recursion](https://33jsconcepts.com/concepts/recursion)** + Learn recursion in JavaScript. Understand base cases, recursive calls, the call stack, and patterns like factorial, tree traversal, and memoization. -- [JavaScript DOM — The Net Ninja](https://www.youtube.com/watch?v=FIORjGvT0kk) -- [JavaScript DOM Crash Course — Traversy Media](https://www.youtube.com/watch?v=0ik6X4DJKCc) -- [JavaScript DOM Manipulation Methods — Web Dev Simplified](https://www.youtube.com/watch?v=y17RuWkWdn8) -- [JavaScript DOM Traversal Methods — Web Dev Simplified](https://www.youtube.com/watch?v=v7rSSy8CaYE) +- **[Currying & Composition](https://33jsconcepts.com/concepts/currying-composition)** + Learn currying and function composition in JavaScript. Build reusable functions from simple pieces using curry, compose, and pipe for cleaner, modular code. -**[⬆ Back to Top](#table-of-contents)** +### Advanced Topics ---- - -## 14. Factories and Classes +- **[JavaScript Engines](https://33jsconcepts.com/concepts/javascript-engines)** + Learn how JavaScript engines work. Understand V8's architecture, parsing, compilation, JIT optimization, hidden classes, inline caching, and garbage collection. -### Articles +- **[Error Handling](https://33jsconcepts.com/concepts/error-handling)** + Learn JavaScript error handling with try/catch/finally. Understand Error types, custom errors, async error patterns, and best practices for robust code. -- [How To Use Classes in JavaScript — Tania Rascia](https://www.digitalocean.com/community/tutorials/understanding-classes-in-javascript) -- [Javascript Classes — Under The Hood — Majid](https://medium.com/tech-tajawal/javascript-classes-under-the-hood-6b26d2667677) -- [Better JavaScript with ES6, Pt. II: A Deep Dive into Classes ― Peleke Sengstacke](https://scotch.io/tutorials/better-javascript-with-es6-pt-ii-a-deep-dive-into-classes) -- [Understand the Factory Design Pattern in Plain JavaScript — Aditya Agarwal](https://medium.com/front-end-hacking/understand-the-factory-design-pattern-in-plain-javascript-20b348c832bd) -- [Factory Functions in JavaScript — Josh Miller](https://atendesigngroup.com/blog/factory-functions-javascript) -- [The Factory Pattern in JS ES6 — SnstsDev](https://medium.com/@SntsDev/the-factory-pattern-in-js-es6-78f0afad17e9) -- [Class vs Factory function: exploring the way forward — Cristi Salcescu](https://medium.freecodecamp.org/class-vs-factory-function-exploring-the-way-forward-73258b6a8d15) -- [How ES6 classes really work and how to build your own — Robert Grosse](https://medium.com/@robertgrosse/how-es6-classes-really-work-and-how-to-build-your-own-fd6085eb326a) -- [Understanding `super` in JavaScript](https://jordankasper.com/understanding-super-in-javascript) -- [An Easy Guide To Understanding Classes In JavaScript](https://dev.to/lawrence_eagles/an-easy-guide-to-understanding-classes-in-javascript-3bcm) +- **[Regular Expressions](https://33jsconcepts.com/concepts/regular-expressions)** + Learn regular expressions in JavaScript. Covers pattern syntax, character classes, quantifiers, flags, capturing groups, and methods like test, match, and replace. -### video Videos +- **[Modern JS Syntax](https://33jsconcepts.com/concepts/modern-js-syntax)** + Learn modern JavaScript ES6+ syntax. Covers destructuring, spread/rest operators, arrow functions, optional chaining, nullish coalescing, and template literals. -- [JavaScript Factory Functions — Programming with Mosh](https://www.youtube.com/watch?v=jpegXpQpb3o) -- [Factory Functions in JavaScript — Fun Fun Function](https://www.youtube.com/watch?v=ImwrezYhw4w) -- [Javascript Tutorial Function Factories — Crypto Chan](https://www.youtube.com/watch?v=R7-IwpH80UE) +- **[ES Modules](https://33jsconcepts.com/concepts/es-modules)** + Learn ES Modules in JavaScript. Understand import/export syntax, why ESM beats CommonJS, live bindings, dynamic imports, top-level await, and how modules enable tree-shaking. -**[⬆ Back to Top](#table-of-contents)** - ---- +- **[Data Structures](https://33jsconcepts.com/concepts/data-structures)** + Learn JavaScript data structures from built-in Arrays, Objects, Maps, and Sets to implementing Stacks, Queues, and Linked Lists. Understand when to use each structure. -## 15. this, call, apply and bind +- **[Algorithms & Big O](https://33jsconcepts.com/concepts/algorithms-big-o)** + Learn Big O notation and algorithms in JavaScript. Understand time complexity, implement searching and sorting algorithms, and recognize common interview patterns. -### Reference +- **[Design Patterns](https://33jsconcepts.com/concepts/design-patterns)** + Learn JavaScript design patterns like Module, Singleton, Observer, Factory, Proxy, and Decorator. Understand when to use each pattern and avoid common pitfalls. -- [call() — MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call) -- [bind() — MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_objects/Function/bind) -- [apply() — MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply) - -### Articles - -- [Grokking call(), apply() and bind() methods in JavaScript — Aniket Kudale](https://levelup.gitconnected.com/grokking-call-apply-and-bind-methods-in-javascript-392351a4be8b) -- [JavaScript's Apply, Call, and Bind Methods are Essential for JavaScript Professionals — Richard Bovell](http://javascriptissexy.com/javascript-apply-call-and-bind-methods-are-essential-for-javascript-professionals/) -- [Javascript: call(), apply() and bind() — Omer Goldberg](https://medium.com/@omergoldberg/javascript-call-apply-and-bind-e5c27301f7bb) -- [The difference between call / apply / bind — Ivan Sifrim](https://medium.com/@ivansifrim/the-differences-between-call-apply-bind-276724bb825b) -- [What the hack is call, apply, bind in JavaScript — Ritik](https://dev.to/ritik_dev_js/what-the-hack-is-call-apply-bind-in-javascript-11ce) -- [Mastering 'this' in JavaScript: Callbacks and bind(), apply(), call() — Michelle Gienow](https://thenewstack.io/mastering-javascript-callbacks-bind-apply-call/) -- [JavaScript's apply, call, and bind explained by hosting a cookout — Kevin Kononenko](https://dev.to/kbk0125/javascripts-apply-call-and-bind-explained-by-hosting-a-cookout-32jo) -- [How AND When to use bind, call, and apply in Javascript — Eigen X](https://www.eigenx.com/blog/https/mediumcom/eigen-x/how-and-when-to-use-bind-call-and-apply-in-javascript-77b6f42898fb) -- [Let me explain to you what is `this`. (Javascript) — Jason Yu](https://dev.to/ycmjason/let-me-explain-to-you-what-is-this-javascript-44ja) -- [Understanding the "this" Keyword in JavaScript — Pavan](https://medium.com/quick-code/understanding-the-this-keyword-in-javascript-cb76d4c7c5e8) -- [How to understand the keyword this and context in JavaScript — Lukas Gisder-Dubé](https://medium.freecodecamp.org/how-to-understand-the-keyword-this-and-context-in-javascript-cd624c6b74b8) -- [What the heck is this in Javascript? — Hridayesh Sharma](https://dev.to/_hridaysharma/what-the-heck-is-this-in-javascript-37n1) -- [This and Bind In Javascript — Brian Barbour](https://dev.to/steelvoltage/this-and-bind-in-javascript-2pam) -- [3 Techniques for Maintaining Your Sanity Using "This" in JavaScript — Carl](https://dev.to/canderson93/3-techniques-for-maintaining-your-sanity-using-this-in-javascript-3idf) -- [Mastering the JavaScript "this" Keyword — Aakash Srivastav](https://dev.to/aakashsr/mastering-the-javascript-this-keyword-4pfa) -- [This binding in JavaScript – 4. New binding — Spyros Argalias](https://dev.to/sargalias/this-binding-in-javascript-4-new-binding-2p1n) -- [A quick intro to 'this' in JavaScript — Natalie Smith](https://dev.to/thatgalnatalie/a-quick-intro-to-this-in-javascript-2mhp) -- [A conversation with the 'this' keyword in Javascript — Karen Efereyan](https://dev.to/developerkaren/a-conversation-with-the-this-keyword-in-javascript-3j6g) -- [What are call(), apply() and bind() in JavaScript — Amitav Mishra](https://jscurious.com/what-are-call-apply-and-bind-in-javascript/) -- [Understanding 'this' binding in JavaScript — Yasemin Cidem](https://yasemincidem.medium.com/understanding-this-binding-in-javascript-86687397c76d) -- [Top 7 tricky questions of 'this' keyword](https://dmitripavlutin.com/javascript-this-interview-questions/) - - -### video Videos - -- [JavaScript call, apply and bind — techsith](https://www.youtube.com/watch?v=c0mLRpw-9rI) -- [JavaScript Practical Applications of Call, Apply and Bind functions— techsith](https://www.youtube.com/watch?v=AYVYxezrMWA) -- [JavaScript (call, bind, apply) — curious aatma](https://www.youtube.com/watch?v=Uy0NOXLBraE) -- [Understanding Functions and 'this' In The World of ES2017 — Bryan Hughes](https://www.youtube.com/watch?v=AOSYY1_np_4) -- [bind and this - Object Creation in JavaScript - FunFunFunction](https://www.youtube.com/watch?v=GhbhD1HR5vk) -- [JS Function Methods call(), apply(), and bind() — Steve Griffith](https://www.youtube.com/watch?v=uBdH0iB1VDM) -- [call, apply and bind method in JavaScript](https://www.youtube.com/watch?v=75W8UPQ5l7k&t=261s) -- .[Javascript Interview Questions ( Call, Bind and Apply ) - Polyfills, Output Based, Explicit Binding - Roadside Coder] (https://youtu.be/VkmUOktYDAU?si=SdvLZ8FBmephPxjS) - -**[⬆ Back to Top](#table-of-contents)** +- **[Clean Code](https://33jsconcepts.com/concepts/clean-code)** + Learn clean code principles for JavaScript. Covers meaningful naming, small functions, DRY, avoiding side effects, and best practices to write maintainable code. --- -## 16. new, Constructor, instanceof and Instances +## Translations -### Articles +This project has been translated into 40+ languages by our amazing community! -- [JavaScript For Beginners: the 'new' operator — Brandon Morelli](https://codeburst.io/javascript-for-beginners-the-new-operator-cee35beb669e) -- [Let's demystify JavaScript's 'new' keyword — Cynthia Lee](https://medium.freecodecamp.org/demystifying-javascripts-new-keyword-874df126184c) -- [Constructor, operator "new" — JavaScript.Info](https://javascript.info/constructor-new) -- [Understanding JavaScript Constructors — Faraz Kelhini](https://css-tricks.com/understanding-javascript-constructors/) -- [Use Constructor Functions — Openclassrooms](https://openclassrooms.com/en/courses/3523231-learn-to-code-with-javascript/4379006-use-constructor-functions) -- [Beyond `typeof` and `instanceof`: simplifying dynamic type checks — Dr. Axel Rauschmayer](http://2ality.com/2017/08/type-right.html) -- [Function and Object, instances of each other — Kiro Risk](https://javascriptrefined.io/function-and-object-instances-of-each-other-1e1095d5faac) -- [JavaScript instanceof operator](https://flexiple.com/javascript/instanceof-javascript) - -**[⬆ Back to Top](#table-of-contents)** +**[View all translations →](TRANSLATIONS.md)** --- -## 17. Prototype Inheritance and Prototype Chain - -### Reference - -- [Inheritance and the prototype chain — MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain) - -### Articles +## Contributing -- [Javascript : Prototype vs Class — Valentin PARSY](https://medium.com/@parsyval/javascript-prototype-vs-class-a7015d5473b) -- [JavaScript engine fundamentals: optimizing prototypes — Mathias Bynens](https://mathiasbynens.be/notes/prototypes) -- [JavaScript Prototype — NC Patro](https://codeburst.io/javascript-prototype-cb29d82b8809) -- [Prototypes in JavaScript — Rupesh Mishra](https://hackernoon.com/prototypes-in-javascript-5bba2990e04b) -- [Prototype in JavaScript: it's quirky, but here's how it works — Pranav Jindal](https://medium.freecodecamp.org/prototype-in-js-busted-5547ec68872) -- [Understanding JavaScript: Prototype and Inheritance — Alexander Kondov](https://hackernoon.com/understanding-javascript-prototype-and-inheritance-d55a9a23bde2) -- [Understanding Classes (ES5) and Prototypal Inheritance in JavaScript — Hridayesh Sharma](https://dev.to/_hridaysharma/understanding-classes-es5-and-prototypal-inheritance-in-javascript-n8d) -- [prototype, **proto** and Prototypal inheritance in JavaScript — Varun Dey](https://dev.to/varundey/prototype-proto-and-prototypal-inheritance-in-javascript-2inl) -- [Prototypal Inheritance — JavaScript.Info](https://javascript.info/prototype-inheritance) -- [How To Work with Prototypes and Inheritance in JavaScript — Tania Rascia](https://www.digitalocean.com/community/tutorials/understanding-prototypes-and-inheritance-in-javascript) -- [Master JavaScript Prototypes & Inheritance — Arnav Aggarwal](https://codeburst.io/master-javascript-prototypes-inheritance-d0a9a5a75c4e) -- [JavaScript's Prototypal Inheritance Explained Using CSS — Nash Vail](https://medium.freecodecamp.org/understanding-prototypal-inheritance-in-javascript-with-css-93b2fcda75e4) -- [Prototypal Inheritance in JavaScript — Jannis Redmann](https://gist.github.com/derhuerst/a585c4916b1c361cc6f0) -- [Demystifying ES6 Classes And Prototypal Inheritance ― Neo Ighodaro](https://scotch.io/tutorials/demystifying-es6-classes-and-prototypal-inheritance) -- [Intro To Prototypal Inheritance — Dharani Jayakanthan](https://dev.to/danny/intro-to-prototypal-inheritance---js-9di) -- [Let's Build Prototypal Inheritance in JS — var-che](https://dev.to/varche/let-s-build-prototypal-inheritance-in-js-56mm) -- [Objects, Prototypes and Classes in JavaScript — Atta](https://dev.to/attacomsian/objects-prototypes-and-classes-in-javascript-3i9b) -- [The magical world of JavaScript prototypes — Belén](https://dev.to/ladybenko/the-magical-world-of-javascript-prototypes-1mhg) -- [Understanding Prototypal Inheritance In JavaScript — Lawrence Eagles](https://dev.to/lawrence_eagles/understanding-prototypal-inheritance-in-javascript-4f31#chp-4) -- [Objects and Prototypes in JavaScript — Irena Popova](https://dev.to/irenejpopova/objects-and-prototypes-in-javascript-2eie) - -### video Videos - -- [Javascript Prototype Inheritance — Avelx](https://www.youtube.com/watch?v=sOrtAjyk4lQ) -- [JavaScript Prototype Inheritance Explained pt. I — techsith](https://www.youtube.com/watch?v=7oNWNlMrkpc) -- [JavaScript Prototype Inheritance Explained pt. II — techsith](https://www.youtube.com/watch?v=uIlj6_z_wL8) -- [JavaScript Prototype Inheritance Explained — Kyle Robinson](https://www.youtube.com/watch?v=qMO-LTOrJaE) -- [Advanced Javascript - Prototypal Inheritance In 1 Minute](https://www.youtube.com/watch?v=G6l5CHl67HQ) -- [An Overview Of Classical Javascript Classes and Prototypal Inheritance — Pentacode](https://www.youtube.com/watch?v=phwzuiJJPpQ) -- [Object Oriented JavaScript - Prototype — The Net Ninja](https://www.youtube.com/watch?v=4jb4AYEyhRc) -- [Prototype in JavaScript — kudvenkat](https://www.youtube.com/watch?v=2rkEbcptR64) -- [JavaScript Using Prototypes — O'Reilly](https://www.youtube.com/watch?v=oCwCcNvaXAQ) -- [A Beginner's Guide to Javascript's Prototype — Tyler Mcginnis](https://www.youtube.com/watch?v=XskMWBXNbp0) -- [Prototypes in Javascript - p5.js Tutorial — The Coding Train](https://www.youtube.com/watch?v=hS_WqkyUah8) - -### Books - -- [You Don't Know JS, 1st Edition: this & Object Prototypes — Kyle Simpson](https://github.com/getify/You-Dont-Know-JS/tree/1st-ed) -- [The Principles of Object-Oriented JavaScript - Nicholas C. Zakas](https://www.google.com.pk/books/edition/The_Principles_of_Object_Oriented_JavaSc/rorlAwAAQBAJ?hl=en&gbpv=1&pg=PP1&printsec=frontcover) - -**[⬆ Back to Top](#table-of-contents)** +We welcome contributions! See our [Contributing Guidelines](CONTRIBUTING.md) for details. --- -## 18. Object.create and Object.assign - -### Reference - -- [Object.create() — MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create) -- [Object.assign() — MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) - -### Articles - -- [Object.create in JavaScript — Rupesh Mishra](https://medium.com/@happymishra66/object-create-in-javascript-fa8674df6ed2) -- [Object.create(): the New Way to Create Objects in JavaScript — Rob Gravelle](https://www.htmlgoodies.com/beyond/javascript/object.create-the-new-way-to-create-objects-in-javascript.html) -- [Basic Inheritance with Object.create — Joshua Clanton](http://adripofjavascript.com/blog/drips/basic-inheritance-with-object-create.html) -- [Object.create() In JavaScript — GeeksforGeeks](https://www.geeksforgeeks.org/object-create-javascript/) -- [Understanding the difference between Object.create() and the new operator — Jonathan Voxland](https://medium.com/@jonathanvox01/understanding-the-difference-between-object-create-and-the-new-operator-b2a2f4749358) -- [JavaScript Object Creation: Patterns and Best Practices — Jeff Mott](https://www.sitepoint.com/javascript-object-creation-patterns-best-practises/) -- [Dealing With Objects in JavaScript With Object.assign, Object.keys and hasOwnProperty](https://www.digitalocean.com/community/tutorials/js-dealing-with-objects) -- [Copying Objects in JavaScript ― Orinami Olatunji](https://scotch.io/bar-talk/copying-objects-in-javascript) -- [JavaScript: Object.assign() — Thiago S. Adriano](https://codeburst.io/javascript-object-assign-bc9696dcbb6e) -- [How to deep clone a JavaScript Object — Flavio Copes](https://flaviocopes.com/how-to-clone-javascript-object/) -- [Object.create(): When and Why to Use — VZing](https://dev.to/vzing/object-create-when-and-why-to-use-20m9) +## License -### video Videos - -- [Object.assign() explained — Aaron Writes Code](https://www.youtube.com/watch?v=aw7NfYhR5rc) -- [Object.assign() Method — techsith](https://www.youtube.com/watch?v=9Ky4X6inpi4) - -**[⬆ Back to Top](#table-of-contents)** +MIT © [Leonardo Maldonado](https://github.com/leonardomso) --- -## 19. map, reduce, filter - -### Articles - -- [JavaScript Functional Programming — map, filter and reduce — Bojan Gvozderac](https://medium.com/jsguru/javascript-functional-programming-map-filter-and-reduce-846ff9ba492d) -- [Learn map, filter and reduce in Javascript — João Miguel Cunha](https://medium.com/@joomiguelcunha/learn-map-filter-and-reduce-in-javascript-ea59009593c4) -- [JavaScript's Map, Reduce, and Filter — Dan Martensen](https://danmartensen.svbtle.com/javascripts-map-reduce-and-filter) -- [How to Use Map, Filter, & Reduce in JavaScript — Peleke Sengstacke](https://code.tutsplus.com/tutorials/how-to-use-map-filter-reduce-in-javascript--cms-26209) -- [JavaScript — Learn to Chain Map, Filter, and Reduce — Brandon Morelli](https://codeburst.io/javascript-learn-to-chain-map-filter-and-reduce-acd2d0562cd4) -- [Javascript data structure with map, reduce, filter and ES6 — Deepak Gupta](https://codeburst.io/write-beautiful-javascript-with-%CE%BB-fp-es6-350cd64ab5bf) -- [Understanding map, filter and reduce in Javascript — Luuk Gruijs](https://hackernoon.com/understanding-map-filter-and-reduce-in-javascript-5df1c7eee464) -- [Functional Programming in JS: map, filter, reduce (Pt. 5) — Omer Goldberg](https://hackernoon.com/functional-programming-in-js-map-filter-reduce-pt-5-308a205fdd5f) -- [JavaScript: Map, Filter, Reduce — William S. Vincent](https://wsvincent.com/functional-javascript-map-filter-reduce/) -- [Arrow Functions: Fat and Concise Syntax in JavaScript — Kyle Pennell](https://www.sitepoint.com/es6-arrow-functions-new-fat-concise-syntax-javascript/) -- [JavaScript: Arrow Functions for Beginners — Brandon Morelli](https://codeburst.io/javascript-arrow-functions-for-beginners-926947fc0cdc) -- [When (and why) you should use ES6 arrow functions — and when you shouldn't — Cynthia Lee](https://medium.freecodecamp.org/when-and-why-you-should-use-es6-arrow-functions-and-when-you-shouldnt-3d851d7f0b26) -- [JavaScript — Learn & Understand Arrow Functions — Brandon Morelli](https://codeburst.io/javascript-learn-understand-arrow-functions-fe2083533946) -- [(JavaScript )=> Arrow functions — sigu](https://medium.com/podiihq/javascript-arrow-functions-27d4c3334b83) -- [Javascript.reduce() — Paul Anderson](https://medium.com/@panderson.dev/javascript-reduce-79aab078da23) -- [Why you should replace forEach with map and filter in JavaScript — Roope Hakulinen](https://gofore.com/en/why-you-should-replace-foreach/) -- [Simplify your JavaScript – Use .map(), .reduce(), and .filter() — Etienne Talbot](https://medium.com/poka-techblog/simplify-your-javascript-use-map-reduce-and-filter-bd02c593cc2d) -- [JavaScript's Reduce Method Explained By Going On a Diet — Kevin Kononenko](https://blog.codeanalogies.com/2018/07/24/javascripts-reduce-method-explained-by-going-on-a-diet/) -- [Difference between map, filter and reduce in JavaScript — Amirata Khodaparast](https://medium.com/@amiratak88/difference-between-map-filter-and-reduce-in-javascript-822ff79d5160) -- [Map⇄Filter⇄Reduce↻ — ashay mandwarya](https://hackernoon.com/map-filter-reduce-ebbed4be4201) -- [Finding Your Way With .map() — Brandon Wozniewicz](https://medium.freecodecamp.org/finding-your-way-with-map-aecb8ca038f6) -- [How to write your own map, filter and reduce functions in JavaScript — Hemand Nair](https://medium.freecodecamp.org/how-to-write-your-own-map-filter-and-reduce-functions-in-javascript-ab1e35679d26) -- [How to Manipulate Arrays in JavaScript — Bolaji Ayodeji](https://www.freecodecamp.org/news/manipulating-arrays-in-javascript/) -- [How to simplify your codebase with map(), reduce(), and filter() in JavaScript — Alex Permyakov](https://www.freecodecamp.org/news/15-useful-javascript-examples-of-map-reduce-and-filter-74cbbb5e0a1f) -- [.map(), .filter(), and .reduce() — Andy Pickle](https://dev.to/pickleat/map-filter-and-reduce-2efb) -- [Map/Filter/Reduce Crash Course — Chris Achard](https://dev.to/chrisachard/map-filter-reduce-crash-course-5gan) -- [Map, Filter and Reduce – Animated — JavaScript Teacher](https://medium.com/@js_tut/map-filter-and-reduce-animated-7fe391a35a47) -- [Map, Filter, Reduce and others Arrays Iterators You Must Know to Become an Algorithms Wizard — Mauro Bono](https://dev.to/uptheirons78/map-filter-reduce-and-others-arrays-iterators-you-must-know-to-become-an-algorithms-wizard-4209) -- [How to Use JavaScript's .map, .filter, and .reduce — Avery Duffin](https://betterprogramming.pub/how-to-javascripts-map-vs-filter-vs-reduce-80d87a5a0a24) -- [Using .map(), .filter() and .reduce() properly — Sasanka Kudagoda](https://medium.com/javascript-in-plain-english/using-map-filter-and-reduce-properly-50e07f80c8b2) -- [Mastering the JavaScript Reduce method ✂️ — sanderdebr](https://dev.to/sanderdebr/mastering-the-javascript-reduce-method-2foj) -- [JavaScript Map – How to Use the JS .map() Function (Array Method) — FreeCodeCamp](https://www.freecodecamp.org/news/javascript-map-how-to-use-the-js-map-function-array-method/) - -### video Videos - -- [Map, Filter and Reduce — Lydia Hallie](https://www.youtube.com/watch?v=UXiYii0Y7Nw) -- [Map, Filter and Reduce - Akshaay Saini](https://youtu.be/zdp0zrpKzIE?si=6QusFzD6tmwn-el4) -- [Functional JavaScript: Map, forEach, Reduce, Filter — Theodore Anderson](https://www.youtube.com/watch?v=vytzLlY_wmU) -- [JavaScript Array superpowers: Map, Filter, Reduce (part I) — Michael Rosata](https://www.youtube.com/watch?v=qTeeVd8hOFY) -- [JavaScript Array superpowers: Map, Filter, Reduce (part 2) — Michael Rosata](https://www.youtube.com/watch?v=gIm9xLYudL0) -- [JavaScript Higher Order Functions - Filter, Map, Sort & Reduce — Epicop](https://www.youtube.com/watch?v=zYBeEPxNSbw) -- [[Array Methods 2/3] .filter + .map + .reduce — CodeWithNick](https://www.youtube.com/watch?v=4qWlqD0yYTU) -- [Arrow functions in JavaScript - What, Why and How — Fun Fun Function](https://www.youtube.com/watch?v=6sQDTgOqh-I) -- [Learning Functional Programming with JavaScript — Anjana Vakil - JSUnconf](https://www.youtube.com/watch?v=e-5obm1G_FY&t=1521s) -- [Map - Parte 2 JavaScript - Fun Fun Function](https://www.youtube.com/watch?v=bCqtb-Z5YGQ&t=17s) -- [Reduce basics - Part 3 of FP in JavaScript - Fun Fun Function](https://www.youtube.com/watch?v=Wl98eZpkp-c) -- [Reduce Advanced - Part 4 of FP in JavaScript - Fun Fun Function](https://www.youtube.com/watch?v=1DMolJ2FrNY&t=621s) -- [reduce Array Method | JavaScript Tutorial - Florin Pop](https://www.youtube.com/watch?v=IXp06KekEjM) -- [map Array Method | JavaScript Tutorial - Florin Pop](https://www.youtube.com/watch?v=P4RAFdZDn3M) -- [Different array methods in 1 minute | Midudev (Spanish)](https://youtu.be/Ah7-PPjQ5Ls) - -**[⬆ Back to Top](#table-of-contents)** - ---- - -## 20. Pure Functions, Side Effects, State Mutation and Event Propagation - -### Articles - -- [Javascript and Functional Programming — Pure Functions — Omer Goldberg](https://hackernoon.com/javascript-and-functional-programming-pt-3-pure-functions-d572bb52e21c) -- [Master the JavaScript Interview: What is a Pure Function? — Eric Elliott](https://medium.com/javascript-scene/master-the-javascript-interview-what-is-a-pure-function-d1c076bec976) -- [JavaScript: What Are Pure Functions And Why Use Them? — James Jeffery](https://medium.com/@jamesjefferyuk/javascript-what-are-pure-functions-4d4d5392d49c) -- [Pure functions in JavaScript — @nicoespeon](http://www.nicoespeon.com/en/2015/01/pure-functions-javascript/) -- [Functional Programming: Pure Functions — Arne Brasseur](https://www.sitepoint.com/functional-programming-pure-functions/) -- [Making your JavaScript Pure — Jack Franklin](https://alistapart.com/article/making-your-javascript-pure) -- [Arrays, Objects and Mutations — Federico Knüssel](https://medium.com/@fknussel/arrays-objects-and-mutations-6b23348b54aa) -- [The State of Immutability — Maciej Sikora](https://medium.com/dailyjs/the-state-of-immutability-169d2cd11310) -- [Hablemos de Inmutabilidad — Kike Sanchez](https://medium.com/zurvin/hablemos-de-inmutabilidad-3dc65d290783) -- [How to deal with dirty side effects in your pure functional JavaScript — James Sinclair](https://jrsinclair.com/articles/2018/how-to-deal-with-dirty-side-effects-in-your-pure-functional-javascript/) -- [Preventing Side Effects in JavaScript — David Walsh](https://davidwalsh.name/preventing-sideeffects-javascript) -- [JavaScript: Pure Functions — William S. Vincent](https://wsvincent.com/javascript-pure-functions/) -- [Functional programming paradigms in modern JavaScript: Pure functions — Alexander Kondov](https://hackernoon.com/functional-programming-paradigms-in-modern-javascript-pure-functions-797d9abbee1) -- [Understanding Javascript Mutation and Pure Functions — Chidume Nnamdi](https://blog.bitsrc.io/understanding-javascript-mutation-and-pure-functions-7231cc2180d3) -- [Functional-ish JavaScript — Daniel Brain](https://medium.com/@bluepnume/functional-ish-javascript-205c05d0ed08) -- [Event Propagation — MDN](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events) -- [Event Propagation — Bubbling and capturing](https://javascript.info/bubbling-and-capturing) - -### video Videos - -- [Pure Functions — Hexlet](https://www.youtube.com/watch?v=dZ41D6LDSBg) -- [Pure Functions - Functional Programming in JavaScript — Paul McBride](https://www.youtube.com/watch?v=Jh_Uzqzz_wM) -- [JavaScript Pure Functions — Seth Alexander](https://www.youtube.com/watch?v=frT3H-eBmPc) -- [JavaScript Pure vs Impure Functions Explained — Theodore Anderson](https://www.youtube.com/watch?v=AHbRVJzpB54) -- [Pure Functions - Programação Funcional: Parte 1 - Fun Fun Function](https://www.youtube.com/watch?v=BMUiFMZr7vk) -- [Event Propagation - JavaScript Event Bubbling and Propagation - Steve Griffith](https://www.youtube.com/watch?v=JYc7gr9Ehl0) - -**[⬆ Back to Top](#table-of-contents)** - ---- - -## 21. Closures - -### Reference - -- [Closures — MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures) -- [Closure — JavaScript.Info](https://javascript.info/closure) - -### Articles - -- [I never understood JavaScript closures — Olivier De Meulder](https://medium.com/dailyjs/i-never-understood-javascript-closures-9663703368e8) -- [Understand JavaScript Closures With Ease — Richard Bovell](http://javascriptissexy.com/understand-javascript-closures-with-ease/) -- [Understanding JavaScript Closures — Codesmith](https://codeburst.io/understanding-javascript-closures-da6aab330302) -- [Understand Closures in JavaScript — Brandon Morelli](https://codeburst.io/understand-closures-in-javascript-d07852fa51e7) -- [A simple guide to help you understand closures in JavaScript — Prashant Ram](https://medium.freecodecamp.org/javascript-closures-simplified-d0d23fa06ba4) -- [Understanding JavaScript Closures: A Practical Approach — Paul Upendo](https://scotch.io/tutorials/understanding-javascript-closures-a-practical-approach) -- [Understanding JavaScript: Closures — Alexander Kondov](https://hackernoon.com/understanding-javascript-closures-4188edf5ea1b) -- [How to use JavaScript closures with confidence — Léna Faure](https://hackernoon.com/how-to-use-javascript-closures-with-confidence-85cd1f841a6b) -- [JavaScript closures by example — tyler](https://howchoo.com/g/mge2mji2mtq/javascript-closures-by-example) -- [JavaScript — Closures and Scope — Alex Aitken](https://codeburst.io/javascript-closures-and-scope-3784c75b9290) -- [Discover the power of closures in JavaScript — Cristi Salcescu](https://medium.freecodecamp.org/discover-the-power-of-closures-in-javascript-5c472a7765d7) -- [Getting Closure — RealLifeJS](http://reallifejs.com/the-meat/getting-closure/) -- [Closure, Currying and IIFE in JavaScript — Ritik](https://dev.to/ritik_dev_js/what-the-hack-is-closure-currying-and-iife-in-javascript-32m9) -- [Understanding Closures in JavaScript — Sukhjinder Arora](https://blog.bitsrc.io/a-beginners-guide-to-closures-in-javascript-97d372284dda) -- [A basic guide to Closures in JavaScript — Parathan Thiyagalingam](https://medium.freecodecamp.org/a-basic-guide-to-closures-in-javascript-9fc8b7e3463e) -- [Closures: Using Memoization — Brian Barbour](https://dev.to/steelvoltage/closures-using-memoization-3597) -- [A Brief Introduction to Closures and Lexical Scoping in JavaScript — Ashutosh K Singh](https://betterprogramming.pub/a-brief-introduction-to-closures-and-lexical-scoping-in-javascript-8a5866496232) -- [Demystify Closures — stereobooster](https://dev.to/stereobooster/demystify-closures-5g42) -- [Scopes and Closures - JavaScript Concepts — Agney Menon](https://dev.to/boywithsilverwings/scopes-and-closures-javascript-concepts-4dfj) -- [Understanding Closures in JavaScript — Matt Popovich](https://dev.to/mattpopovich/understanding-closures-in-javascript-3k0d) -- [whatthefuck.is · A Closure - Dan Abramov](https://whatthefuck.is/closure) -- [Closures in JavaScript can... - Brandon LeBoeuf](https://dev.to/brandonleboeuf/closure-in-javascript-49n7) -- [Do you know Closures - Mohamed Khaled](https://dev.to/this_mkhy/do-you-know-es6-part-3-advanced-3fcl#Closures-2) - -### video Videos - -- [JavaScript The Hard Parts: Closure, Scope & Execution Context - Codesmith](https://www.youtube.com/watch?v=XTAzsODSCsM) -- [Namaste Javascript by Akshay Saini](https://youtu.be/qikxEIxsXco?si=fGFgUHuaOW49Wg9p) -- [Javascript Closure — techsith](https://www.youtube.com/watch?v=71AtaJpJHw0) -- [Closures — Fun Fun Function](https://www.youtube.com/watch?v=CQqwU2Ixu-U) -- [Closures in JavaScript — techsith](https://www.youtube.com/watch?v=-xqJo5VRP4A) -- [JavaScript Closures 101: What is a closure? — JavaScript Tutorials](https://www.youtube.com/watch?v=yiEeiMN2Khs) -- [Closures — freeCodeCamp](https://www.youtube.com/watch?v=1JsJx1x35c0) -- [JavaScript Closures — CodeWorkr](https://www.youtube.com/watch?v=-rLrGAXK8WE) -- [Closures in JS - Akshay Saini](https://www.youtube.com/watch?v=qikxEIxsXco) -- [CLOSURES en JavaScript: Qué son y cómo funcionan - Carlos Azaustre](https://youtu.be/xa8lhVwQBw4) -- [Learn Closures In 7 Minutes - Web Dev Simplified](https://www.youtube.com/watch?v=3a0I8ICR1Vg) - - -**[⬆ Back to Top](#table-of-contents)** - ---- - -## 22. High Order Functions - -### Books - -- [Eloquent JavaScript, 3rd Edition: Ch. 5 - Higher-order Functions](https://eloquentjavascript.net/05_higher_order.html) - -### Articles - -- [Higher-Order Functions in JavaScript — M. David Green](https://www.sitepoint.com/higher-order-functions-javascript/) -- [Higher Order Functions: Using Filter, Map and Reduce for More Maintainable Code — Guido Schmitz](https://medium.freecodecamp.org/higher-order-functions-in-javascript-d9101f9cf528) -- [First-class and Higher Order Functions: Effective Functional JavaScript — Hugo Di Francesco](https://hackernoon.com/effective-functional-javascript-first-class-and-higher-order-functions-713fde8df50a) -- [Higher Order Functions in JavaScript — John Hannah](https://www.lullabot.com/articles/higher-order-functions-in-javascript) -- [Just a reminder on how to use high order functions — Pedro Filho](https://github.com/pedroapfilho/high-order-functions) -- [Understanding Higher-Order Functions in JavaScript — Sukhjinder Arora](https://blog.bitsrc.io/understanding-higher-order-functions-in-javascript-75461803bad) -- [Higher Order Functions - A pragmatic approach — emmanuel ikwuoma](https://dev.to/nuel_ikwuoma/higher-order-functions-a-pragmatic-approach-51fb) - -### video Videos - -- [JavaScript Higher Order Functions & Arrays — Traversy Media](https://www.youtube.com/watch?v=rRgD1yVwIvE) -- [Higher Order Functions — Fun Fun Function](https://www.youtube.com/watch?v=BMUiFMZr7vk) -- [Higher Order Functions in Javascript — Raja Yogan](https://www.youtube.com/watch?v=dTlpYnmBW9I) -- [Higher Order Iterators in JavaScript — Fun Fun Function](https://www.youtube.com/watch?v=GYRMNp1SKXA) -- [Higher Order Functions in JavaScript — The Coding Train](https://www.youtube.com/watch?v=H4awPsyugS0) -- [Part 1: An Introduction to Callbacks and Higher Order Functions - Codesmith](https://www.youtube.com/watch?v=7E8ctomPQJw) -- [Part 2: Understanding Why We Need Higher Order Functions - Codesmith](https://www.youtube.com/watch?v=28MXziDZkE4) -- [Higher-Order Functions ft. Functional Programming - Akshay Saini](https://www.youtube.com/watch?v=HkWxvB1RJq0) - -**[⬆ Back to Top](#table-of-contents)** - ---- - -## 23. Recursion - -### Articles - -- [Recursion in JavaScript — Kevin Ennis](https://medium.freecodecamp.org/recursion-in-javascript-1608032c7a1f) -- [Understanding Recursion in JavaScript — Zak Frisch](https://medium.com/@zfrisch/understanding-recursion-in-javascript-992e96449e03) -- [Learn and Understand Recursion in JavaScript — Brandon Morelli](https://codeburst.io/learn-and-understand-recursion-in-javascript-b588218e87ea) -- [Recursion in Functional JavaScript — M. David Green](https://www.sitepoint.com/recursion-functional-javascript/) -- [Programming with JS: Recursion — Alexander Kondov](https://hackernoon.com/programming-with-js-recursion-31371e2bf808) -- [Anonymous Recursion in JavaScript — simo](https://dev.to/simov/anonymous-recursion-in-javascript) -- [Recursion, iteration and tail calls in JS — loverajoel](http://www.jstips.co/en/javascript/recursion-iteration-and-tail-calls-in-js/) -- [What is Recursion? A Recursive Function Explained with JavaScript Code Examples — Nathan Sebhastian](https://www.freecodecamp.org/news/what-is-recursion-in-javascript/) -- [Intro to Recursion — Brad Newman](https://medium.com/@newmanbradm/intro-to-recursion-984a8bd50f4b) -- [Accio Recursion!: Your New Favorite JavaScript Spell — Leanne Cabey](https://medium.datadriveninvestor.com/accio-recursion-your-new-favorite-javascript-spell-7e10d3125fb3) -- [Recursion Explained (with Examples) — Christina](https://dev.to/christinamcmahon/recursion-explained-with-examples-4k1m) - -### video Videos - -- [Recursion In JavaScript — techsith](https://www.youtube.com/watch?v=VtG0WAUvq2w) -- [Recursion — Fun Fun Function](https://www.youtube.com/watch?v=k7-N8R0-KY4) -- [Recursion and Recursive Functions — Hexlet](https://www.youtube.com/watch?v=vLhHyGTkjCs) -- [Recursion: Recursion() — JS Monthly — Lucas da Costa](https://www.youtube.com/watch?v=kGXVsd8pBLw) -- [Recursive Function in JavaScript — kudvenkat](https://www.youtube.com/watch?v=uyjsR9eNTIw) -- [What on Earth is Recursion? — Computerphile](https://www.youtube.com/watch?v=Mv9NEXX1VHc) -- [Javascript Tutorial 34: Introduction To Recursion — codedamn](https://www.youtube.com/watch?v=9NO5dXSlbv8) -- [Recursion, Iteration, and JavaScript: A Love Story | JSHeroes 2018 — Anjana Vakil](https://www.youtube.com/watch?v=FmiQr4nfoPQ) -- [Recursion crash course - Colt Steele](https://www.youtube.com/watch?v=lMBVwYrmFZQ&ab_channel=ColtSteele) -- [What Is Recursion - In Depth - Web Dev Simplified](https://www.youtube.com/watch?v=6oDQaB2one8) - -**[⬆ Back to Top](#table-of-contents)** - ---- - -## 24. Collections and Generators - -### Reference - -- [Generator — MDN web docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator) - -### Articles - -- [ES6 Collections: Using Map, Set, WeakMap, WeakSet — Kyle Pennell](https://www.sitepoint.com/es6-collections-map-set-weakmap-weakset/) -- [ES6 WeakMaps, Sets, and WeakSets in Depth — Nicolás Bevacqua](https://ponyfoo.com/articles/es6-weakmaps-sets-and-weaksets-in-depth) -- [Map, Set, WeakMap and WeakSet — JavaScript.Info](https://javascript.info/map-set-weakmap-weakset) -- [Maps in ES6 - A Quick Guide — Ben Mildren](https://dev.to/mildrenben/maps-in-es6---a-quick-guide-35pk) -- [ES6 — Set vs Array — What and when? — Maya Shavin](https://medium.com/front-end-hacking/es6-set-vs-array-what-and-when-efc055655e1a) -- [ES6 — Map vs Object — What and when? — Maya Shavin](https://medium.com/front-end-hacking/es6-map-vs-object-what-and-when-b80621932373) -- [Array vs Set vs Map vs Object — Real-time use cases in Javascript (ES6/ES7) — Rajesh Babu](https://codeburst.io/array-vs-set-vs-map-vs-object-real-time-use-cases-in-javascript-es6-47ee3295329b) -- [How to create an array of unique values in JavaScript using Sets — Claire Parker-Jones](https://dev.to/claireparker/how-to-create-an-array-of-unique-values-in-javascript-using-sets-5dg6) -- [What You Should Know About ES6 Maps — Just Chris](https://hackernoon.com/what-you-should-know-about-es6-maps-dc66af6b9a1e) -- [ES6 Maps in Depth — Nicolás Bevacqua](https://ponyfoo.com/articles/es6-maps-in-depth) -- [What are JavaScript Generators and how to use them — Vladislav Stepanov](https://codeburst.io/what-are-javascript-generators-and-how-to-use-them-c6f2713fd12e) -- [Understanding JavaScript Generators With Examples — Arfat Salman](https://codeburst.io/understanding-generators-in-es6-javascript-with-examples-6728834016d5) -- [The Basics of ES6 Generators — Kyle Simpson](https://davidwalsh.name/es6-generators) -- [An Introduction to JavaScript Generators — Alice Kallaugher](https://dev.to/kallaugher/an-introduction-to-javascript-generators-1224) - -### video Videos - -- [JavaScript ES6 / ES2015 Set, Map, WeakSet and WeakMap — Traversy Media](https://www.youtube.com/watch?v=ycohYSx5h9w) -- [JavaScript ES6 / ES2015 - \[11\] Generators - Traversy Media](https://www.youtube.com/watch?v=dcP039DYzmE) -- [The Differences between ES6 Maps and Sets — Steve Griffith](https://www.youtube.com/watch?v=m4abICrldQI) -- [Javascript Generators - THEY CHANGE EVERYTHING - ES6 Generators Harmony Generators — LearnCode.academy](https://www.youtube.com/watch?v=QO07THdLWQo) - -**[⬆ Back to Top](#table-of-contents)** - ---- - -## 25. Promises - -### Reference - -- [Promise — MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) - -### Articles - -- [JavaScript Promises for Dummies ― Jecelyn Yeen](https://scotch.io/tutorials/javascript-promises-for-dummies) -- [Understanding promises in JavaScript — Gokul N K](https://hackernoon.com/understanding-promises-in-javascript-13d99df067c1) -- [Master the JavaScript Interview: What is a Promise? — Eric Elliott](https://medium.com/javascript-scene/master-the-javascript-interview-what-is-a-promise-27fc71e77261) -- [An Overview of JavaScript Promises — Sandeep Panda](https://www.sitepoint.com/overview-javascript-promises/) -- [How to use Promises in JavaScript — Prashant Ram](https://medium.freecodecamp.org/promises-in-javascript-explained-277b98850de) -- [Implementing Promises In JavaScript — Maciej Cieslar](https://medium.freecodecamp.org/how-to-implement-promises-in-javascript-1ce2680a7f51) -- [JavaScript: Promises explained with simple real life analogies — Shruti Kapoor](https://codeburst.io/javascript-promises-explained-with-simple-real-life-analogies-dd6908092138) -- [Promises for Asynchronous Programming — Exploring JS](http://exploringjs.com/es6/ch_promises.html) -- [JavaScript Promises Explained By Gambling At A Casino — Kevin Kononenko](https://blog.codeanalogies.com/2018/08/26/javascript-promises-explained-by-gambling-at-a-casino/) -- [ES6 Promises: Patterns and Anti-Patterns — Bobby Brennan](https://medium.com/datafire-io/es6-promises-patterns-and-anti-patterns-bbb21a5d0918) -- [A Simple Guide to ES6 Promises — Brandon Morelli](https://codeburst.io/a-simple-guide-to-es6-promises-d71bacd2e13a) -- [The ES6 Promises — Manoj Singh Negi](https://codeburst.io/the-es6-promises-87a979ab27e4) -- [ES6 Promises in Depth — Nicolás Bevacqua](https://ponyfoo.com/articles/es6-promises-in-depth) -- [Playing with Javascript Promises: A Comprehensive Approach — Rajesh Babu](https://codeburst.io/playing-with-javascript-promises-a-comprehensive-approach-25ab752c78c3) -- [How to Write a JavaScript Promise — Brandon Wozniewicz](https://medium.freecodecamp.org/how-to-write-a-javascript-promise-4ed8d44292b8) -- [A Coding Writer's Guide: An Introduction To ES6 Promises — Andrew Ly](https://medium.com/@andrewly07/a-coding-writers-guide-an-introduction-to-es6-promises-9ff9f9e88f6c) -- [Understanding Promises in JavaScript — Chris Noring](https://dev.to/itnext/reverse-engineering-understand-promises-1jfc) -- [Converting callbacks to promises — Zell Liew](https://dev.to/zellwk/converting-callbacks-to-promises-nhn) -- [JavaScript Promises: Zero To Hero Plus Cheat Sheet — Joshua Saunders](https://medium.com/dailyjs/javascript-promises-zero-to-hero-plus-cheat-sheet-64d75051cffa) -- [Promises - JavaScript concepts — Agney Menon](https://dev.to/boywithsilverwings/promises-javascript-concepts-293c) -- [Javascript `Promise` 101 — Igor Irianto](https://dev.to/iggredible/javascript-promise-101-3idl) -- [Simplify JavaScript Promises — Sunny Singh](https://dev.to/sunnysingh/simplify-javascript-promises-4djb) -- [JavaScript Visualized: Promises & Async/Await — Lydia Hallie](https://dev.to/lydiahallie/javascript-visualized-promises-async-await-5gke) -- [Promises in JavaScript — Peter Klingelhofer](https://dev.to/peterklingelhofer/promises-in-javascript-3h5k) -- [Best Practices for ES6 Promises — Basti Ortiz](https://dev.to/somedood/best-practices-for-es6-promises-36da) -- [Lo que debemos saber de EScript 2020 — Kike Sanchez](https://medium.com/zurvin/lo-que-debemos-saber-de-escript-2020-5fc61da5e4cd) -- [Promise Basics - javascript.info](https://javascript.info/promise-basics) -- [The Complete JavaScript Promise Guide](https://blog.webdevsimplified.com/2021-09/javascript-promises) -- [Promise Chaining - javascript.info](https://javascript.info/promise-chaining) - -### video Videos - -- [Let's Learn ES6 - Promises — Ryan Christiani](https://www.youtube.com/watch?v=vQ3MoXnKfuQ) -- [JavaScript ES6 / ES2015 Promises — Traversy Media](https://www.youtube.com/watch?v=XJEHuBZQ5dU) -- [Promises — Fun Fun Function](https://www.youtube.com/watch?v=2d7s3spWAzo) -- [Error Handling Promises in JavaScript — Fun Fun Function](https://www.youtube.com/watch?v=f8IgdnYIwOU) -- [Promises Part 1 - Topics of JavaScript/ES6 — The Coding Train](https://www.youtube.com/watch?v=QO4NXhWo_NM) -- [JavaScript Promise in 100 Seconds](https://www.youtube.com/watch?v=RvYYCGs45L4) -- [JavaScript Promise in 9 Minutes](https://youtu.be/3NjdOtHpcBM) -- [JavaScript Promises In 10 Minutes — Web Dev Simplified ](https://www.youtube.com/watch?v=DHvZLI7Db8E) -- [Promises | Ep 02 Season 02 - Namaste JavaScript - Akshay Saini ](https://youtu.be/ap-6PPAuK1Y?si=Ri1fopXeYjlrHzpf) - -**[⬆ Back to Top](#table-of-contents)** - ---- - -## 26. async/await - -### Reference - -- [async/await — JavaScript.Info](https://javascript.info/async-await) - -### Books - -- [Eloquent JavaScript, 3rd Edition: Ch. 11 - Asynchronous Programming](https://eloquentjavascript.net/11_async.html) -- [Exploring JS: Asynchronous Programming](http://exploringjs.com/es6/ch_async.html) - -### Articles - -- [Understanding async/await in Javascript — Gokul N K](https://hackernoon.com/understanding-async-await-in-javascript-1d81bb079b2c) -- [Asynchronous Javascript using async/await — Joy Warugu](https://scotch.io/tutorials/asynchronous-javascript-using-async-await) -- [Modern Asynchronous JavaScript with async/await — Flavio Copes](https://flaviocopes.com/javascript-async-await/) -- [Javascript — ES8 Introducing async/await Functions — Ben Garrison](https://medium.com/@_bengarrison/javascript-es8-introducing-async-await-functions-7a471ec7de8a) -- [How to escape async/await hell — Aditya Agarwal](https://medium.freecodecamp.org/avoiding-the-async-await-hell-c77a0fb71c4c) -- [Understanding JavaScript's async await — Nicolás Bevacqua](https://ponyfoo.com/articles/understanding-javascript-async-await) -- [JavaScript Async/Await: Serial, Parallel and Complex Flow — TechBrij](https://techbrij.com/javascript-async-await-parallel-sequence) -- [From JavaScript Promises to Async/Await: why bother? — Chris Nwamba](https://blog.pusher.com/promises-async-await/) -- [Flow Control in Modern JS: Callbacks to Promises to Async/Await — Craig Buckler](https://www.sitepoint.com/flow-control-callbacks-promises-async-await/) -- [How to improve your asynchronous Javascript code with async and await — Indrek Lasn](https://medium.freecodecamp.org/improve-your-asynchronous-javascript-code-with-async-and-await-c02fc3813eda) -- [Making Fetches Easy With Async Await — Mickey Sheridan](https://medium.com/@micksheridan.24/making-fetches-easy-with-async-await-8a1246efa1f6) -- [7 Reasons Why JavaScript Async/Await Is Better Than Plain Promises — Mostafa Gaafar](https://dev.to/gafi/7-reasons-to-always-use-async-await-over-plain-promises-tutorial-4ej9) -- [Asynchronous Operations in JavaScript — Jscrambler](https://dev.to/jscrambler/asynchronous-operations-in-javascript-2p6b) -- [JavaScript: Promises or async-await — Gokul N K](https://medium.com/better-programming/should-i-use-promises-or-async-await-126ab5c98789) -- [Async / Await: From Zero to Hero — Zhi Yuan](https://dev.to/zhiyuanamos/async-await-from-zero-to-hero-a22) -- [JavaScript Visualized: Promises & Async/Await — Lydia Hallie](https://dev.to/lydiahallie/javascript-visualized-promises-async-await-5gke) -- [Making asynchronous programming easier with async and await — MDN](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Async_await) -- [JavaScript Async/Await Tutorial – Learn Callbacks, Promises, and Async/Await in JS by Making Ice Cream](https://www.freecodecamp.org/news/javascript-async-await-tutorial-learn-callbacks-promises-async-await-by-making-icecream/) -- [Better Than Promises - JavaScript Async/Await](https://blog.webdevsimplified.com/2021-11/async-await/) - -### video Videos - -- [Asynchronous JavaScript Crash Course](https://www.youtube.com/watch?v=exBgWAIeIeg) -- [Async + Await — Wes Bos](https://www.youtube.com/watch?v=9YkUCxvaLEk) -- [Asynchrony: Under the Hood — Shelley Vohr](https://www.youtube.com/watch?v=SrNQS8J67zc) -- [async/await in JavaScript - What, Why and How — Fun Fun Function](https://www.youtube.com/watch?v=568g8hxJJp4&index=3&list=PL0zVEGEvSaeHJppaRLrqjeTPnCH6) -- [async/await Part 1 - Topics of JavaScript/ES8 — The Coding Train](https://www.youtube.com/watch?v=XO77Fib9tSI&index=3&list=PLRqwX-V7Uu6bKLPQvPRNNE65kBL62mVfx) -- [async/await Part 2 - Topics of JavaScript/ES8 — The Coding Train](https://www.youtube.com/watch?v=chavThlNz3s&index=4&list=PLRqwX-V7Uu6bKLPQvPRNNE65kBL62mVfx) -- [Complete Guide to JS Async & Await ES2017/ES8 — Colt Steele](https://www.youtube.com/watch?v=krAYA4rvbdA) -- [Tips for using async/await in JavaScript — James Q Quick](https://www.youtube.com/watch?v=_9vgd9XKlDQ) -- [JavaScript Async Await — Web Dev Simplified](https://www.youtube.com/watch?v=V_Kr9OSfDeU) -- [Promise async and await in javascript — Hitesh Choudhary](https://youtu.be/Gjbr21JLfgg?si=SDCVKr9ONw2GsNdT) - -**[⬆ Back to Top](#table-of-contents)** - ---- - -## 27. Data Structures - -### Articles - -- [Data Structures in JavaScript — Thon Ly](https://medium.com/siliconwat/data-structures-in-javascript-1b9aed0ea17c) -- [Algorithms and Data Structures in JavaScript — Oleksii Trekhleb](https://itnext.io/algorithms-and-data-structures-in-javascript-a71548f902cb) -- [Data Structures: Objects and Arrays ― Chris Nwamba](https://scotch.io/courses/10-need-to-know-javascript-concepts/data-structures-objects-and-arrays) -- [Data structures in JavaScript — Benoit Vallon](http://blog.benoitvallon.com/data-structures-in-javascript/data-structures-in-javascript/) -- [Playing with Data Structures in Javascript — Anish K.](https://blog.cloudboost.io/playing-with-data-structures-in-javascript-stack-a55ebe50f29d) -- [The Little Guide of Queue in JavaScript — Germán Cutraro](https://hackernoon.com/the-little-guide-of-queue-in-javascript-4f67e79260d9) -- [All algorithms writing with JavaScript in the book 'Algorithms Fourth Edition'](https://github.com/barretlee/algorithms) -- [Collection of classic computer science paradigms in JavaScript](https://github.com/nzakas/computer-science-in-javascript) -- [All the things you didn't know you wanted to know about data structures](https://github.com/jamiebuilds/itsy-bitsy-data-structures) -- [JavaScript Data Structures: 40 Part Series — miku86](https://dev.to/miku86/series/3259) -- [Data Structures: Understanding Graphs — Rachel Hawa](https://medium.com/javascript-in-plain-english/data-structures-understanding-graphs-82509d35e6b5) -- [Data Structures Two Ways: Linked List (Pt 1) — Freddie Duffield](https://dev.to/freddieduffield/data-structures-two-ways-linked-list-2n61) -- [Data Structures Two Ways: Linked List (Pt 2) — Freddie Duffield](https://dev.to/freddieduffield/data-structures-two-ways-linked-list-pt2-2i60) -- [Graph Data Structures Explained in JavaScript — Adrian Mejia](https://dev.to/amejiarosario/graph-data-structures-for-beginners-5edn) - -### video Videos - -- [Algorithms In Javascript | Ace Your Interview — Eduonix Learning Solutions](https://www.youtube.com/watch?v=H_EBPZgiAas&list=PLDmvslp_VR0zYUSth_8O69p4_cmvZEgLa) -- [Data Structures and Algorithms in JavaScript — freeCodeCamp](https://www.youtube.com/watch?v=Gj5qBheGOEo&list=PLWKjhJtqVAbkso-IbgiiP48n-O-JQA9PJ) -- [Learning JavaScript Data Structures and Algorithms: Sorting — Packt Video](https://www.youtube.com/watch?v=Ymh_AurrMbA) -- [JavaScript Data Structures: Getting Started — Academind](https://www.youtube.com/watch?v=41GSinwoMYA&ab_channel=Academind) - -**[⬆ Back to Top](#table-of-contents)** - ---- - -## 28. Expensive Operation and Big O Notation - -### Articles - -- [Big O Notation in Javascript — César Antón Dorantes](https://medium.com/cesars-tech-insights/big-o-notation-javascript-25c79f50b19b) -- [Time Complexity/Big O Notation — Tim Roberts](https://medium.com/javascript-scene/time-complexity-big-o-notation-1a4310c3ee4b) -- [Big O in JavaScript — Gabriela Medina](https://medium.com/@gmedina229/big-o-in-javascript-36ff67766051) -- [Big O Search Algorithms in JavaScript — Bradley Braithwaite](https://www.bradoncode.com/blog/2012/04/big-o-algorithm-examples-in-javascript.html) -- [Algorithms in plain English: time complexity and Big-O Notation — Michael Olorunnisola](https://medium.freecodecamp.org/time-is-complex-but-priceless-f0abd015063c) -- [An Introduction to Big O Notation — Joseph Trettevik](https://dev.to/lofiandcode/an-introduction-to-big-o-notation-210o) - -### video Videos - -- [JavaScript: Intro to Big O Notation and Function Runtime — Eric Traub](https://www.youtube.com/watch?v=HgA5VOFan5E) -- [Essential Big O for JavaScript Developers — Dave Smith](https://www.youtube.com/watch?v=KatlvCFHPRo) -- [Big O Notation - Time Complexity Analysis — WebTunings](https://www.youtube.com/watch?v=ALl86xJiTD8) -- [Learn Big O Notation In 12 Minutes - Web Dev Simplified](https://www.youtube.com/watch?v=itn09C2ZB9Y) -- [JavaScript Algorithms: Big-O Notation - Codevolution](https://www.youtube.com/watch?v=3yUuo7TqMW8) -- [JavaScript Algorithms Crash Course: Learn Algorithms & "Big O" from the Ground Up! - Academind](https://www.youtube.com/watch?v=JgWm6sQwS_I) -- [Big O Notation - Data Structures and Algorithms in Javascript - RoadSideCoder](https://www.youtube.com/watch?v=LaexPVi1VRE) - -**[⬆ Back to Top](#table-of-contents)** - ---- - -## 29. Algorithms - -### Articles - -- [Data Structures and Algorithms using ES6](https://github.com/Crizstian/data-structure-and-algorithms-with-ES6) -- [Algorithms and data structures implemented in JavaScript with explanations and links to further readings](https://github.com/trekhleb/javascript-algorithms) -- [JS: Interview Algorithm](http://www.thatjsdude.com/interview/js1.html) -- [Algorithms in JavaScript — Thon Ly](https://medium.com/siliconwat/algorithms-in-javascript-b0bed68f4038) -- [JavaScript Objects, Square Brackets and Algorithms — Dmitri Grabov](https://medium.freecodecamp.org/javascript-objects-square-brackets-and-algorithms-e9a2916dc158) -- [Atwood's Law applied to CS101 - Classic algorithms and data structures implemented in JavaScript](https://github.com/felipernb/algorithms.js) -- [Data Structures and Algorithms library in JavaScript](https://github.com/yangshun/lago) -- [Collection of computer science algorithms and data structures written in JavaScript](https://github.com/idosela/algorithms-in-javascript) -- [Algorithms and Data Structures in JavaScript — Oleksii Trekhleb](https://dev.to/trekhleb/algorithms-and-data-structures-in-javascript-49i3) - -### video Videos - -- 🎥 [JavaScript Algorithms - Codevolution](https://www.youtube.com/playlist?list=PLC3y8-rFHvwiRYB4-HHKHblh3_bQNJTMa) -- 🎥 [Dynamic Programming - Learn to Solve Algorithmic Problems & Coding Challenges - FreeCodeCamp](https://www.youtube.com/watch?v=oBt53YbR9Kk&t=1021s) -- 🎥 [Data Structures and Algorithms in Javascript | DSA with JS - RoadsideCoder](https://www.youtube.com/playlist?list=PLKhlp2qtUcSZtJefDThsXcsAbRBCSTgW4) -- 🎥 [Javascript Algorithms + Data Structures - KodingKevin](https://www.youtube.com/playlist?list=PLn2ipk-jqgZiAHiA70hOxAj8RMUeqYNK3) -- 🎥 [JavaScript Data Structures: Getting Started - Academind](https://www.youtube.com/watch?v=41GSinwoMYA) -- 🎥 [Algorithms and Data Structures - The Coding Train (Daniel Shiffman)](https://www.youtube.com/playlist?list=PLRqwX-V7Uu6ZiZxtDDRCi6uhfTH4FilpH) - -**[⬆ Back to Top](#table-of-contents)** - ---- - -## 30. Inheritance, Polymorphism and Code Reuse - -### Reference - -- [Inheritance in JavaScript — MDN](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Inheritance) -- [Class inheritance, super — JavaScript.Info](https://javascript.info/class-inheritance) - -### Articles - -- [Inheritance in JavaScript — Rupesh Mishra](https://hackernoon.com/inheritance-in-javascript-21d2b82ffa6f) -- [Simple Inheritance with JavaScript — David Catuhe](https://www.sitepoint.com/simple-inheritance-javascript/) -- [JavaScript — Inheritance, delegation patterns and Object linking — NC Patro](https://codeburst.io/javascript-inheritance-25fe61ab9f85) -- [Object Oriented JavaScript: Polymorphism with examples — Knoldus Blogs](https://blog.knoldus.com/object-oriented-javascript-polymorphism-with-examples/) -- [Program Like Proteus — A beginner's guide to polymorphism in Javascript — Sam Galson](https://medium.com/yld-blog/program-like-proteus-a-beginners-guide-to-polymorphism-in-javascript-867bea7c8be2) -- [Object-oriented JavaScript: A Deep Dive into ES6 Classes — Jeff Mott](https://www.sitepoint.com/object-oriented-javascript-deep-dive-es6-classes/) -- [Unlocking the Power of Polymorphism in JavaScript: A Deep Dive](https://prototypr.io/post/unlocking-the-power-of-polymorphism-in-javascript-a-deep-dive) - -### video Videos - -- [Inheritance in JavaScript — kudvenkat](https://www.youtube.com/watch?v=yXlFR81tDBM) -- [JavaScript ES6 Classes and Inheritance — Traversy Media](https://www.youtube.com/watch?v=RBLIm5LMrmc) -- [Polymorphism in JavaScript — kudvenkat](https://www.youtube.com/watch?v=zdovG9cuEBA) - -**[⬆ Back to Top](#table-of-contents)** - ---- - -## 31. Design Patterns - -### Books - -- [Learning JavaScript Design Patterns — Addy Osmani](https://addyosmani.com/resources/essentialjsdesignpatterns/book/) -- [Pro JavaScript Design Patterns — Ross Harmes and Dustin Diaz](https://pepa.holla.cz/wp-content/uploads/2016/08/Pro-JavaScript-Design-Patterns.pdf) - -### Articles - -- [JavaScript Design Patterns – Explained with Examples — Germán Cocca](https://www.freecodecamp.org/news/javascript-design-patterns-explained/) -- [4 JavaScript Design Patterns You Should Know — Devan Patel](https://scotch.io/bar-talk/4-javascript-design-patterns-you-should-know) -- [JavaScript Design Patterns – Beginner's Guide to Mobile Web Development — Soumyajit Pathak](https://medium.com/beginners-guide-to-mobile-web-development/javascript-design-patterns-25f0faaaa15) -- [JavaScript Design Patterns — Akash Pal](https://medium.com/front-end-hacking/javascript-design-patterns-ed9d4c144c81) -- [JavaScript Design Patterns: Understanding Design Patterns in JavaScript - Sukhjinder Arora](https://blog.bitsrc.io/understanding-design-patterns-in-javascript-13345223f2dd) -- [All the 23 (GoF) design patterns implemented in Javascript — Felipe Beline](https://github.com/fbeline/Design-Patterns-JS) -- [The Power of the Module Pattern in JavaScript — jsmanifest](https://medium.com/better-programming/the-power-of-the-module-pattern-in-javascript-3c73f7cd10e8) -- [Design Patterns for Developers using JavaScript pt. I — Oliver Mensah](https://dev.to/omensah/design-patterns-for-developers-using-javascript----part-one--b3e) -- [Design Patterns for Developers using JavaScript pt. II — Oliver Mensah](https://dev.to/omensah/design-patterns-for-developers-using-javascript---part-two--3p39) -- [Design patterns in modern JavaScript development](https://levelup.gitconnected.com/design-patterns-in-modern-javascript-development-ec84d8be06ca) -- [Understanding Design Patterns: Iterator using Dev.to and Medium social networks! — Carlos Caballero](https://dev.to/carlillo/understanding-design-patterns-iterator-using-dev-to-and-medium-social-networks-3bdd) -- [JavaScript Design Patterns - Factory Pattern — KristijanFištrek](https://dev.to/kristijanfistrek/javascript-design-patterns-factory-pattern-562p) -- [JavaScript Design Pattern — Module Pattern - Factory Pattern — Moon](https://medium.com/javascript-in-plain-english/javascript-design-pattern-module-pattern-555737eccecd) -- [Design Patterns: Null Object - Carlos Caballero](https://medium.com/better-programming/design-patterns-null-object-5ee839e37892) -- [Strategy Pattern - Francesco Ciulla](https://dev.to/francescoxx/strategy-pattern-5oh) -- [Adapter Pattern - Francesco Ciulla](https://dev.to/francescoxx/adapter-pattern-5bjk) -- [The Power of Composite Pattern in JavaScript - jsmanifest](https://dev.to/jsmanifest/the-power-of-composite-pattern-in-javascript-2732) -- [In Defense of Defensive Programming - Adam Nathaniel Davis](https://dev.to/bytebodger/in-defense-of-defensive-programming-k45) -- [JavaScript Patterns Workshop — Lydia Hallie](https://javascriptpatterns.vercel.app/patterns) - -### video Videos - -- [JavaScript Design Patterns — Udacity](https://www.udacity.com/course/javascript-design-patterns--ud989) -- [JavaScript Patterns for 2017 — Scott Allen](https://www.youtube.com/watch?v=hO7mzO83N1Q) - -**[⬆ Back to Top](#table-of-contents)** - ---- - -## 32. Partial Applications, Currying, Compose and Pipe - -### Books - -- [Functional-Light JavaScript: Ch. 3 - Managing Function Inputs — Kyle Simpson](https://github.com/getify/Functional-Light-JS/blob/master/manuscript/ch3.md) - -### Articles - -- [Composition and Currying Elegance in JavaScript — Pragyan Das](https://medium.com/@pragyan88/writing-middleware-composition-and-currying-elegance-in-javascript-8b15c98a541b) -- [Functional JavaScript: Function Composition For Every Day Use — Joel Thoms](https://hackernoon.com/javascript-functional-composition-for-every-day-use-22421ef65a10) -- [Functional Composition: compose() and pipe() — Anton Paras](https://medium.com/@acparas/what-i-learned-today-july-2-2017-ab9a46dbf85f) -- [Why The Hipsters Compose Everything: Functional Composing In JavaScript — A. Sharif](http://busypeoples.github.io/post/functional-composing-javascript/) -- [A Gentle Introduction to Functional JavaScript pt III: Functions for making functions — James Sinclair](https://jrsinclair.com/articles/2016/gentle-introduction-to-functional-javascript-functions/) -- [Curry And Compose (why you should be using something like ramda in your code) — jsanchesleao](https://jsleao.wordpress.com/2015/02/22/curry-and-compose-why-you-should-be-using-something-like-ramda-in-your-code/) -- [Function Composition in JavaScript with Pipe — Andy Van Slaars](https://vanslaars.io/post/create-pipe-function/) -- [Practical Functional JavaScript with Ramda — Andrew D'Amelio, Yuri Takhteyev](https://developer.telerik.com/featured/practical-functional-javascript-ramda/) -- [The beauty in Partial Application, Currying, and Function Composition — Joel Thoms](https://hackernoon.com/the-beauty-in-partial-application-currying-and-function-composition-d885bdf0d574) -- [Curry or Partial Application? — Eric Elliott](https://medium.com/javascript-scene/curry-or-partial-application-8150044c78b8) -- [Partial Application in JavaScript — Ben Alman](http://benalman.com/news/2012/09/partial-application-in-javascript/) -- [Partial Application of Functions — Functional Reactive Ninja](https://hackernoon.com/partial-application-of-functions-dbe7d9b80760) -- [Partial Application in ECMAScript 2015 — Ragan Wald](http://raganwald.com/2015/04/01/partial-application.html) -- [So You Want to be a Functional Programmer pt. I — Charles Scalfani](https://medium.com/@cscalfani/so-you-want-to-be-a-functional-programmer-part-1-1f15e387e536) -- [So You Want to be a Functional Programmer pt. II — Charles Scalfani](https://medium.com/@cscalfani/so-you-want-to-be-a-functional-programmer-part-2-7005682cec4a) -- [So You Want to be a Functional Programmer pt. III — Charles Scalfani](https://medium.com/@cscalfani/so-you-want-to-be-a-functional-programmer-part-3-1b0fd14eb1a7) -- [So You Want to be a Functional Programmer pt. IV — Charles Scalfani](https://medium.com/@cscalfani/so-you-want-to-be-a-functional-programmer-part-4-18fbe3ea9e49) -- [So You Want to be a Functional Programmer pt. V — Charles Scalfani](https://medium.com/@cscalfani/so-you-want-to-be-a-functional-programmer-part-5-c70adc9cf56a) -- [An introduction to the basic principles of Functional Programming — TK](https://medium.freecodecamp.org/an-introduction-to-the-basic-principles-of-functional-programming-a2c2a15c84) -- [Concepts of Functional Programming in javascript — TK](https://medium.com/the-renaissance-developer/concepts-of-functional-programming-in-javascript-6bc84220d2aa) -- [An Introduction to Functional Programming Style in JavaScript — JavaScript Teacher](https://medium.freecodecamp.org/an-introduction-to-functional-programming-style-in-javascript-71fcc050f064) -- [A practical guide to writing more functional JavaScript — Nadeesha Cabral](https://medium.freecodecamp.org/a-practical-guide-to-writing-more-functional-javascript-db49409f71) -- [A simple explanation of functional pipe in JavaScript — Ben Lesh](https://dev.to/benlesh/a-simple-explanation-of-functional-pipe-in-javascript-2hbj) - -### video Videos - -- [Compose vs Pipe: Functional Programming in JavaScript — Chyld Studios](https://www.youtube.com/watch?v=Wl2ejJOqHUU) -- [JavaScript Functional Programing: Compose — Theodore Anderson](https://www.youtube.com/watch?v=jigHxo9YR30) -- [Function Composition - Functional JavaScript — NWCalvank](https://www.youtube.com/watch?v=mth5WpEc4Qs) -- [JavaScript Function Composition Explained — Theodore Anderson](https://www.youtube.com/watch?v=Uam37AlzPYw) -- [Let's code with function composition — Fun Fun Function](https://www.youtube.com/watch?v=VGB9HbL1GHk) -- [Partial Application vs. Currying — NWCalvank](https://www.youtube.com/watch?v=DzLkRsUN2vE) -- [JavaScript Partial Application — Theodore Anderson](https://www.youtube.com/watch?v=jkebgHEcvac) -- [call, apply and bind method in JavaScript](https://www.youtube.com/watch?v=75W8UPQ5l7k&t=261s) - -**[⬆ Back to Top](#table-of-contents)** - ---- - -## 33. Clean Code - -### Articles - -- [Clean Code Explained – A Practical Introduction to Clean Coding for Beginners — freeCodeCamp](https://www.freecodecamp.org/news/clean-coding-for-beginners/) -- [Clean Code concepts adapted for JavaScript — Ryan McDermott](https://github.com/ryanmcdermott/clean-code-javascript) -- [Function parameters in JavaScript Clean Code — Kevin Peters](https://medium.com/@kevin_peters/function-parameters-in-javascript-clean-code-4caac109159b) -- [Keeping your code clean — Samuel James](https://codeburst.io/keeping-your-code-clean-d30bcffd1a10) -- [Best Practices for Using Modern JavaScript Syntax — M. David Green](https://www.sitepoint.com/modern-javascript-best-practices/) -- [best practices for cross node/web development - Jimmy Wärting](https://github.com/cross-js/cross-js) -- [Writing Clean Code - Dylan Paulus](https://dev.to/ganderzz/on-writing-clean-code-57cm) -- [Writing Clean Code and The Practice of Programming - Nityesh Agarwal](https://dev.to/nityeshaga/writing-clean-code-and-the-practice-of-programming-actionable-advice-for-beginners-5f0k) -- [Clean code, dirty code, human code - Daniel Irvine](https://dev.to/d_ir/clean-code-dirty-code-human-code-6nm) -- [Practical Ways to Write Better JavaScript - Ryland G](https://dev.to/taillogs/practical-ways-to-write-better-javascript-26d4) -- [The Must-Know Clean Code Principles - Kesk on Medium](https://medium.com/swlh/the-must-know-clean-code-principles-1371a14a2e75) -- [The Clean Code Book - Robert C Martin](https://www.amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882/) -- [How to use destructuring in JavaScript to write cleaner, more powerful code - freecodecamp](https://www.freecodecamp.org/news/how-to-use-destructuring-in-javascript-to-write-cleaner-more-powerful-code-9d1b38794050/) -- [Write Clean Code Using JavaScript Object Destructuring - Asel Siriwardena](https://betterprogramming.pub/write-clean-code-using-javascript-object-destructuring-3551302130e7) - -### video Videos - -- 🎥 [JavaScript Pro Tips - Code This, NOT That](https://www.youtube.com/watch?v=Mus_vwhTCq0) -- 🎥 [Clean Code playlist - Beau teaches](https://www.youtube.com/watch?v=b9c5GmmS7ks&list=PLWKjhJtqVAbkK24EaPurzMq0-kw5U9pJh&index=1) -- 🎥 [JavaScript Best Practices and Coding Conventions - Write Clean Code](https://youtu.be/RMN_bkZ1KM0?si=Ssg3cNZ_DB7CIwKQ) -- 🎥 [JavaScript Clean Code](https://youtu.be/vPXzVNmCPg4?si=QR1k4E6Zx5H4mfcs) -- 🎥 [Tips On Learning How To Code](https://www.youtube.com/watch?v=0wHyoBPc6zs) - -**[⬆ Back to Top](#table-of-contents)** - -## License - -This software is licensed under MIT License. See [License](https://github.com/leonardomso/33-js-concepts/blob/master/LICENSE) for more information ©Leonardo Maldonado. - -**[⬆ Back to Top](#table-of-contents)** - -
-
- Happy Learning! ⭐ -

If you find this repository helpful, please consider giving it a star!

+ If you find this helpful, please star the repo!
diff --git a/TRANSLATIONS.md b/TRANSLATIONS.md new file mode 100644 index 00000000..98374658 --- /dev/null +++ b/TRANSLATIONS.md @@ -0,0 +1,54 @@ +# Translations + +This project has been translated into 40+ languages thanks to our amazing community of contributors. + +## Available Translations + +- [اَلْعَرَبِيَّةُ‎ (Arabic)](https://github.com/amrsekilly/33-js-concepts) — Amr Elsekilly +- [Български (Bulgarian)](https://github.com/thewebmasterp/33-js-concepts) — thewebmasterp +- [汉语 (Chinese)](https://github.com/stephentian/33-js-concepts) — Re Tian +- [Português do Brasil (Brazilian Portuguese)](https://github.com/tiagoboeing/33-js-concepts) — Tiago Boeing +- [한국어 (Korean)](https://github.com/yjs03057/33-js-concepts.git) — Suin Lee +- [Español (Spanish)](https://github.com/adonismendozaperez/33-js-conceptos) — Adonis Mendoza +- [Türkçe (Turkish)](https://github.com/ilker0/33-js-concepts) — İlker Demir +- [русский язык (Russian)](https://github.com/gumennii/33-js-concepts) — Mihail Gumennii +- [Tiếng Việt (Vietnamese)](https://github.com/nguyentranchung/33-js-concepts) — Nguyễn Trần Chung +- [Polski (Polish)](https://github.com/lip3k/33-js-concepts) — Dawid Lipinski +- [فارسی (Persian)](https://github.com/majidalavizadeh/33-js-concepts) — Majid Alavizadeh +- [Bahasa Indonesia (Indonesian)](https://github.com/rijdz/33-js-concepts) — Rijdzuan Sampoerna +- [Français (French)](https://github.com/robinmetral/33-concepts-js) — Robin Métral +- [हिन्दी (Hindi)](https://github.com/vikaschauhan/33-js-concepts) — Vikas Chauhan +- [Ελληνικά (Greek)](https://github.com/DimitrisZx/33-js-concepts) — Dimitris Zarachanis +- [日本語 (Japanese)](https://github.com/oimo23/33-js-concepts) — oimo23 +- [Deutsch (German)](https://github.com/burhannn/33-js-concepts) — burhannn +- [украї́нська мо́ва (Ukrainian)](https://github.com/AndrewSavetchuk/33-js-concepts-ukrainian-translation) — Andrew Savetchuk +- [සිංහල (Sinhala)](https://github.com/ududsha/33-js-concepts) — Udaya Shamendra +- [Italiano (Italian)](https://github.com/Donearm/33-js-concepts) — Gianluca Fiore +- [Latviešu (Latvian)](https://github.com/ANormalStick/33-js-concepts) — Jānis Īvāns +- [Afaan Oromoo (Oromo)](https://github.com/Amandagne/33-js-concepts) — Amanuel Dagnachew +- [ภาษาไทย (Thai)](https://github.com/ninearif/33-js-concepts) — Arif Waram +- [Català (Catalan)](https://github.com/marioestradaf/33-js-concepts) — Mario Estrada +- [Svenska (Swedish)](https://github.com/FenixHongell/33-js-concepts/) — Fenix Hongell +- [ខ្មែរ (Khmer)](https://github.com/Chhunneng/33-js-concepts) — Chrea Chanchhunneng +- [አማርኛ (Ethiopian)](https://github.com/hmhard/33-js-concepts) — Miniyahil Kebede (ምንያህል ከበደ) +- [Беларуская мова (Belarussian)](https://github.com/Yafimau/33-js-concepts) — Dzianis Yafimau +- [O'zbekcha (Uzbek)](https://github.com/smnv-shokh/33-js-concepts) — Shokhrukh Usmonov +- [Urdu (اردو)](https://github.com/sudoyasir/33-js-concepts) — Yasir Nawaz +- [हिन्दी (Hindi)](https://github.com/milostivyy/33-js-concepts) — Mahima Chauhan +- [বাংলা (Bengali)](https://github.com/Jisan-mia/33-js-concepts) — Jisan Mia +- [ગુજરાતી (Gujarati)](https://github.com/VatsalBhuva11/33-js-concepts) — Vatsal Bhuva +- [سنڌي (Sindhi)](https://github.com/Sunny-unik/33-js-concepts) — Sunny Gandhwani +- [भोजपुरी (Bhojpuri)](https://github.com/debnath003/33-js-concepts) — Pronay Debnath +- [ਪੰਜਾਬੀ (Punjabi)](https://github.com/Harshdev098/33-js-concepts) — Harsh Dev Pathak +- [Latin (Latin)](https://github.com/Harshdev098/33-js-concepts) — Harsh Dev Pathak +- [മലയാളം (Malayalam)](https://github.com/Stark-Akshay/33-js-concepts) — Akshay Manoj +- [Yorùbá (Yoruba)](https://github.com/ayobaj/33-js-concepts) — Ayomide Bajulaye +- [עברית‎ (Hebrew)](https://github.com/rafyzg/33-js-concepts) — Refael Yzgea +- [Nederlands (Dutch)](https://github.com/dlvisser/33-js-concepts) — Dave Visser +- [தமிழ் (Tamil)](https://github.com/UdayaKrishnanM/33-js-concepts) — Udaya Krishnan M + +--- + +## Want to Translate? + +We'd love to have more translations! See our [Contributing Guidelines](CONTRIBUTING.md) for details on how to submit a translation. diff --git a/docs/concepts/algorithms-big-o.mdx b/docs/concepts/algorithms-big-o.mdx new file mode 100644 index 00000000..db6665c9 --- /dev/null +++ b/docs/concepts/algorithms-big-o.mdx @@ -0,0 +1,660 @@ +--- +title: "Algorithms & Big O: Measuring Code Performance in JavaScript" +sidebarTitle: "Algorithms & Big O: Measuring Code Performance" +description: "Learn Big O notation and algorithms in JavaScript. Understand time complexity, implement searching and sorting algorithms, and recognize common interview patterns." +--- + +Why does one solution pass all tests instantly while another times out? Why do interviewers care so much about "time complexity"? Consider these two functions that both find if an array contains duplicates: + +```javascript +// Approach A: Nested loops +function hasDuplicatesA(arr) { + for (let i = 0; i < arr.length; i++) { + for (let j = i + 1; j < arr.length; j++) { + if (arr[i] === arr[j]) return true + } + } + return false +} + +// Approach B: Using a Set +function hasDuplicatesB(arr) { + return new Set(arr).size !== arr.length +} +``` + +Both work correctly. But with 100,000 elements, Approach A takes several seconds while Approach B finishes in milliseconds. The difference? **[Big O notation](https://en.wikipedia.org/wiki/Big_O_notation)**, which tells us how code performance scales with input size. + + +**What you'll learn in this guide:** +- What Big O notation actually measures +- The common complexities: O(1), O(log n), O(n), O(n log n), O(n²) +- How to analyze your own code's complexity +- JavaScript built-in methods and their complexity +- Implementing binary search and merge sort +- Common interview patterns: two pointers, sliding window, frequency counter + + + +**Prerequisite:** This guide assumes you're familiar with [data structures](/concepts/data-structures) like arrays, objects, Maps, and Sets. You should also be comfortable with [recursion](/concepts/recursion) for the sorting algorithms section. + + +--- + +## What is Big O Notation? + +**Big O notation** describes how an algorithm's runtime or space requirements grow as input size increases. It provides an upper bound on growth rate and ignores constants, giving us a way to compare algorithms regardless of hardware. + +### The Package Delivery Analogy + +Imagine you need to deliver packages to houses on a street: + +- **O(1)**: You have the exact address. Go directly there. Whether there are 10 or 10,000 houses, it takes the same time. +- **O(n)**: You check each house until you find the right one. More houses = proportionally more time. +- **O(n²)**: For each house, you compare it with every other house. 10 houses = 100 comparisons. 100 houses = 10,000 comparisons. + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ HOW ALGORITHMS SCALE │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Operations │ +│ ▲ │ +│ │ O(n²) │ +│ 1M │ •••• │ +│ │ ••• │ +│ │ ••• │ +│ │ ••• │ +│ 100K │ •••• O(n log n) │ +│ │ ••• ════════════ │ +│ │ •••• ═════ │ +│ │ •••• ═════ │ +│ 10K │ ••••• ═════ O(n) │ +│ │ •••• ═════ ────────────── │ +│ │ •••• ═════ ─────── │ +│ │ •••• ═════ ─────── O(log n) │ +│ 1K │•••• ═════ ────── ............ │ +│ │═════ ─────── ........ │ +│ │ ────── .......... O(1) │ +│ 100 │──── .......... ══════════════ │ +│ └──────────────────────────────────────────────────────────► │ +│ 100 1K 10K 100K 1M Input (n) │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## The Big O Complexity Scale + +Here are the most common complexities you'll encounter, from fastest to slowest: + +| Complexity | Name | Example | 1,000 items | 1,000,000 items | +|------------|------|---------|-------------|-----------------| +| O(1) | Constant | Array access | 1 op | 1 op | +| O(log n) | Logarithmic | Binary search | ~10 ops | ~20 ops | +| O(n) | Linear | Simple loop | 1,000 ops | 1,000,000 ops | +| O(n log n) | Linearithmic | Merge sort | ~10,000 ops | ~20,000,000 ops | +| O(n²) | Quadratic | Nested loops | 1,000,000 ops | 1,000,000,000,000 ops | + + + + The operation takes the same time regardless of input size. + + ```javascript + // Array access by index + const arr = [1, 2, 3, 4, 5] + const element = arr[2] // O(1) - instant, no matter array size + + // Object/Map lookup + const user = { name: 'Alice', age: 30 } + const name = user.name // O(1) + + const map = new Map() + map.set('key', 'value') + map.get('key') // O(1) + + // Array push and pop (end operations) + arr.push(6) // O(1) + arr.pop() // O(1) + ``` + + + + Time increases slowly as input grows. Each step eliminates half the remaining data. This is the "sweet spot" for searching sorted data. + + ```javascript + // Binary search - covered in detail below + // With 1,000,000 elements, only ~20 comparisons needed! + + // Think of it like a phone book: + // Open middle → wrong half eliminated → repeat + ``` + + + + Time grows proportionally with input. If you double the input, you double the time. + + ```javascript + // Finding maximum value + function findMax(arr) { + let max = arr[0] + for (let i = 1; i < arr.length; i++) { // Visits each element once + if (arr[i] > max) max = arr[i] + } + return max + } + + // Most array methods are O(n) + arr.indexOf(5) // O(n) - may check every element + arr.includes(5) // O(n) + arr.find(x => x > 3) // O(n) + arr.filter(x => x > 3) // O(n) + arr.map(x => x * 2) // O(n) + ``` + + + + Common for efficient sorting algorithms. Faster than O(n²), but slower than O(n). + + ```javascript + // JavaScript's built-in sort is O(n log n) + const sorted = [...arr].sort((a, b) => a - b) + + // Merge sort and quick sort (average case) are also O(n log n) + ``` + + + + Time grows with the square of input. Nested loops over the same data are the typical culprit. **Avoid for large datasets.** + + ```javascript + // Checking all pairs + function findPairs(arr) { + const pairs = [] + for (let i = 0; i < arr.length; i++) { // O(n) + for (let j = i + 1; j < arr.length; j++) { // O(n) for each i + pairs.push([arr[i], arr[j]]) + } + } + return pairs // Total: O(n) × O(n) = O(n²) + } + + // Bubble sort - O(n²), mostly used for teaching + ``` + + + +--- + +## How to Analyze Your Code + +Follow these steps to determine your code's complexity: + + + + What variable represents n? Usually it's array length or a number parameter. + + + + - One loop over n elements = O(n) + - Nested loops = multiply: O(n) × O(n) = O(n²) + - Loop that halves each time = O(log n) + + + + - O(2n) → O(n) + - O(n² + n) → O(n²) + - O(500) → O(1) + + + +```javascript +// Example analysis +function example(arr) { + console.log(arr[0]) // O(1) + + for (let i = 0; i < arr.length; i++) { // O(n) + console.log(arr[i]) + } + + for (let i = 0; i < arr.length; i++) { // O(n) + for (let j = 0; j < arr.length; j++) { // × O(n) + console.log(arr[i], arr[j]) + } + } +} +// Total: O(1) + O(n) + O(n²) = O(n²) +// The n² dominates, so we say this function is O(n²) +``` + +--- + +## JavaScript Built-in Methods Complexity + +Knowing these helps you make better decisions: + +### Array Methods + +| Method | Complexity | Why | +|--------|------------|-----| +| `push()`, `pop()` | O(1) | End operations, no shifting | +| `shift()`, `unshift()` | O(n) | Must re-index all elements | +| `splice()` | O(n) | May shift elements | +| `slice()` | O(n) | Creates copy of portion | +| `indexOf()`, `includes()` | O(n) | Linear search | +| `find()`, `findIndex()` | O(n) | Linear search | +| `map()`, `filter()`, `forEach()` | O(n) | Visits each element | +| `sort()` | O(n log n) | Implementation varies by browser | + +### Object, Map, and Set + +| Operation | Object | Map | Set | +|-----------|--------|-----|-----| +| Get/Set/Has | O(1) | O(1) | O(1) | +| Delete | O(1) | O(1) | O(1) | +| Keys/Values | O(n) | O(n) | O(n) | + + +**Use Set.has() instead of Array.includes()** when checking membership repeatedly. Set lookups are O(1) while array searches are O(n). + + +--- + +## Searching Algorithms + +### Linear Search - O(n) + +Check each element one by one. Simple but slow for large arrays. + +```javascript +function linearSearch(arr, target) { + for (let i = 0; i < arr.length; i++) { + if (arr[i] === target) return i + } + return -1 +} + +linearSearch([3, 7, 1, 9, 4], 9) // Returns 3 +``` + +### Binary Search - O(log n) + +Divide and conquer on a **sorted** array. Eliminates half the remaining elements each step. + +```javascript +function binarySearch(arr, target) { + let left = 0 + let right = arr.length - 1 + + while (left <= right) { + const mid = Math.floor((left + right) / 2) + + if (arr[mid] === target) return mid + if (arr[mid] < target) left = mid + 1 + else right = mid - 1 + } + + return -1 +} + +binarySearch([1, 3, 5, 7, 9, 11, 13], 9) // Returns 4 +``` + + +**Binary search requires a sorted array.** If your data isn't sorted, you'll need to sort it first O(n log n) or use linear search. + + +--- + +## Sorting Algorithms + +### Quick Reference + +| Algorithm | Best | Average | Worst | Space | Use When | +|-----------|------|---------|-------|-------|----------| +| Bubble Sort | O(n)* | O(n²) | O(n²) | O(1) | Never in production | +| Merge Sort | O(n log n) | O(n log n) | O(n log n) | O(n) | Need guaranteed performance | +| Quick Sort | O(n log n) | O(n log n) | O(n²) | O(log n)** | General purpose | +| JS `sort()` | O(n log n) | O(n log n) | O(n log n) | O(n) | Most cases | + +*Bubble sort achieves O(n) best case only with early termination optimization (when no swaps needed). +**Quick sort space is O(log n) average case, O(n) worst case due to recursion stack depth. + +### Bubble Sort - O(n²) + +Repeatedly swaps adjacent elements if they're in wrong order. Educational, but too slow for real use. + +```javascript +function bubbleSort(arr) { + const result = [...arr] + const n = result.length + + for (let i = 0; i < n; i++) { + let swapped = false + + for (let j = 0; j < n - i - 1; j++) { + if (result[j] > result[j + 1]) { + [result[j], result[j + 1]] = [result[j + 1], result[j]] + swapped = true + } + } + + // If no swaps occurred, array is sorted + if (!swapped) break + } + + return result +} +``` + +### Merge Sort - O(n log n) + +Divide array in half, sort each half, merge them back. Consistent performance with guaranteed O(n log n). + +```javascript +function mergeSort(arr) { + if (arr.length <= 1) return arr + + const mid = Math.floor(arr.length / 2) + const left = mergeSort(arr.slice(0, mid)) + const right = mergeSort(arr.slice(mid)) + + return merge(left, right) +} + +function merge(left, right) { + const result = [] + let i = 0 + let j = 0 + + while (i < left.length && j < right.length) { + if (left[i] <= right[j]) { + result.push(left[i]) + i++ + } else { + result.push(right[j]) + j++ + } + } + + return result.concat(left.slice(i)).concat(right.slice(j)) +} + +mergeSort([38, 27, 43, 3, 9, 82, 10]) // [3, 9, 10, 27, 38, 43, 82] +``` + + +**In practice, use JavaScript's built-in `sort()`**. Modern browsers typically use Timsort (V8) or similar O(n log n) algorithms optimized for real-world data. Implement your own sorts for learning or when you have specific requirements. + + +--- + +## Common Interview Patterns + +These patterns solve many algorithm problems efficiently. + +### Two Pointers - O(n) + +Use two pointers moving toward each other or in the same direction. Great for sorted arrays and finding pairs. + +```javascript +// Find pair that sums to target in sorted array +function twoSum(arr, target) { + let left = 0 + let right = arr.length - 1 + + while (left < right) { + const sum = arr[left] + arr[right] + + if (sum === target) return [left, right] + if (sum < target) left++ + else right-- + } + + return null +} + +twoSum([1, 3, 5, 7, 9], 12) // [1, 4] (3 + 9 = 12) +``` + +### Sliding Window - O(n) + +Maintain a "window" that slides through the array. Perfect for subarray problems. + +```javascript +// Maximum sum of k consecutive elements +function maxSumSubarray(arr, k) { + if (arr.length < k) return null + + // Calculate first window + let windowSum = 0 + for (let i = 0; i < k; i++) { + windowSum += arr[i] + } + + let maxSum = windowSum + + // Slide the window: remove left element, add right element + for (let i = k; i < arr.length; i++) { + windowSum = windowSum - arr[i - k] + arr[i] + maxSum = Math.max(maxSum, windowSum) + } + + return maxSum +} + +maxSumSubarray([2, 1, 5, 1, 3, 2], 3) // 9 (5 + 1 + 3) +``` + +### Frequency Counter - O(n) + +Use an object or Map to count occurrences. Avoids nested loops when comparing collections. + +```javascript +// Check if two strings are anagrams +function isAnagram(str1, str2) { + if (str1.length !== str2.length) return false + + const freq = {} + + // Count characters in first string + for (const char of str1) { + freq[char] = (freq[char] || 0) + 1 + } + + // Subtract counts for second string + for (const char of str2) { + if (!freq[char]) return false + freq[char]-- + } + + return true +} + +isAnagram('listen', 'silent') // true +isAnagram('hello', 'world') // false +``` + +--- + +## Key Takeaways + + +**The key things to remember:** + +1. **Big O measures scalability**, not absolute speed. It tells you how performance changes as input grows. + +2. **O(1) and O(log n) are fast**. O(n) is acceptable. O(n²) gets slow quickly. Avoid O(2^n) for any significant input. + +3. **Nested loops multiply complexity**. Two nested loops over n = O(n²). Three = O(n³). + +4. **Drop constants and lower terms**. O(2n + 100) simplifies to O(n). + +5. **Array end operations are O(1)**, beginning operations are O(n). Prefer `push`/`pop` over `shift`/`unshift`. + +6. **Use Set for O(1) lookups** instead of `Array.includes()` for repeated membership checks. + +7. **Binary search is O(log n)** but requires sorted data. Worth sorting first if you'll search multiple times. + +8. **JavaScript's sort() is O(n log n)** in all modern browsers. Use it unless you have specific requirements. + +9. **Learn the patterns**: Two pointers, sliding window, and frequency counter solve most interview problems. + +10. **Space complexity matters too**. Creating a new array of size n uses O(n) space. + + +--- + +## Test Your Knowledge + + + + ```javascript + function mystery(arr) { + for (let i = 0; i < arr.length; i++) { + for (let j = 0; j < 10; j++) { + console.log(arr[i]) + } + } + } + ``` + + **Answer:** O(n) + + The outer loop runs n times, but the inner loop always runs exactly 10 times (constant). So it's O(n × 10) = O(10n) = O(n). The constant 10 is dropped. + + + + **Answer:** Because each comparison eliminates half the remaining elements. + + With 1,000 elements: 1000 → 500 → 250 → 125 → 62 → 31 → 15 → 7 → 3 → 1 + + That's about 10 steps. log₂(1000) ≈ 10. With 1,000,000 elements, it only takes ~20 steps. + + + + **Answer:** It depends on how many lookups you need. + + - **One lookup**: `indexOf()` is faster. O(n) vs O(n) for Set creation + O(1) for lookup. + - **Many lookups**: Convert to Set first. O(n) creation + O(1) per lookup beats O(n) per lookup. + + Rule of thumb: If you'll search more than once, use a Set. + + + + **Answer:** It's O(n²), making it impractical for large datasets. + + With 10,000 elements: ~100,000,000 operations. JavaScript's built-in sort() at O(n log n) would take ~130,000 operations for the same data. + + Bubble sort is useful for learning but should never be used in production code. + + + + **Answer:** Use a Set to track seen elements: + + ```javascript + function hasDuplicates(arr) { + const seen = new Set() + for (const item of arr) { + if (seen.has(item)) return true // O(1) lookup + seen.add(item) // O(1) insert + } + return false + } + ``` + + This is O(n) time and O(n) space. The naive nested loop approach would be O(n²) time but O(1) space. + + + + **Answer:** **Sliding window** with a Set or Map. + + ```javascript + function longestUniqueSubstring(s) { + const seen = new Set() + let maxLen = 0 + let left = 0 + + for (let right = 0; right < s.length; right++) { + while (seen.has(s[right])) { + seen.delete(s[left]) + left++ + } + seen.add(s[right]) + maxLen = Math.max(maxLen, right - left + 1) + } + + return maxLen + } + ``` + + Time: O(n), Space: O(min(n, alphabet size)) + + + +--- + +## Related Concepts + + + + Understanding arrays, objects, Maps, and Sets is essential for choosing the right tool + + + Many algorithms like merge sort and binary search can be implemented recursively + + + Map, filter, and reduce are built on these concepts + + + Understanding the complexity of these common operations + + + +--- + +## Reference + + + + Complete reference for array methods and their behavior + + + Hash-based key-value storage with fast lookups + + + O(1) operations for membership testing + + + +## Articles + + + + Visual reference for time and space complexity of common algorithms and data structures. Bookmark this one. + + + Comprehensive GitHub repo with 190k+ stars. Every algorithm implemented in JavaScript with explanations. + + + Detailed breakdown of every array method's complexity with examples and explanations. + + + FreeCodeCamp's beginner-friendly guide to Big O with real-world analogies. + + + +## Videos + + + + Web Dev Simplified's concise explanation. Perfect if you want the essentials without filler. + + + Codevolution's complete series covering sorting, searching, and common patterns step by step. + + + FreeCodeCamp's comprehensive 8-hour course. Great for deep learning when you have the time. + + diff --git a/docs/concepts/async-await.mdx b/docs/concepts/async-await.mdx new file mode 100644 index 00000000..4b9599d1 --- /dev/null +++ b/docs/concepts/async-await.mdx @@ -0,0 +1,1622 @@ +--- +title: "async/await: Writing Async Code That Looks Synchronous in JavaScript" +sidebarTitle: "async/await: Writing Async Code That Looks Synchronous" +description: "Learn async/await in JavaScript. Syntactic sugar over Promises that makes async code readable. Covers error handling with try/catch, parallel execution with Promise.all, and common pitfalls." +--- + +Why does asynchronous code have to look so complicated? What if you could write code that fetches data from a server, waits for user input, or reads files, all while looking as clean and readable as regular synchronous code? + +```javascript +// This is async code that reads like sync code +async function getUserData(userId) { + const response = await fetch(`/api/users/${userId}`) + const user = await response.json() + return user +} + +// Using the async function +(async () => { + const user = await getUserData(123) + console.log(user.name) // "Alice" +})() +``` + +That's the magic of **[async/await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function)**. It's syntactic sugar introduced in ES2017 that makes asynchronous JavaScript look and behave like synchronous code, while still being non-blocking under the hood. + + +**What you'll learn in this guide:** +- What async/await actually is (and why it's "just" Promises underneath) +- How the `async` keyword transforms functions into Promise-returning functions +- How `await` pauses execution without blocking the main thread +- Error handling with try/catch (finally, a sane way to handle async errors!) +- The critical difference between sequential and parallel execution +- The most common async/await mistakes and how to avoid them +- How async/await relates to the event loop and microtasks + + + +**Prerequisites:** This guide assumes you understand [Promises](/concepts/promises). async/await is built entirely on top of them. You should also be familiar with the [Event Loop](/concepts/event-loop) to understand why code after `await` behaves like a microtask. + + +--- + +## What is async/await? + +Think of **async/await** as a friendlier way to write [Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). You mark a function with `async`, use `await` to pause until a Promise resolves, and your async code suddenly reads like regular synchronous code. The best part? JavaScript stays non-blocking under the hood. + +Here's the same operation written three ways: + + + + ```javascript + // Callback hell - nested so deep you need a flashlight + function getUserPosts(userId, callback) { + fetchUser(userId, (err, user) => { + if (err) return callback(err) + + fetchPosts(user.id, (err, posts) => { + if (err) return callback(err) + + fetchComments(posts[0].id, (err, comments) => { + if (err) return callback(err) + + callback(null, { user, posts, comments }) + }) + }) + }) + } + ``` + + + ```javascript + // Promise chains - better, but still nested + function getUserPosts(userId) { + return fetchUser(userId) + .then(user => { + return fetchPosts(user.id) + .then(posts => { + return fetchComments(posts[0].id) + .then(comments => ({ user, posts, comments })) + }) + }) + } + ``` + + + ```javascript + // async/await - reads like synchronous code! + async function getUserPosts(userId) { + const user = await fetchUser(userId) + const posts = await fetchPosts(user.id) + const comments = await fetchComments(posts[0].id) + return { user, posts, comments } + } + ``` + + + +The async/await version is much easier to read. Each line clearly shows what happens next, error handling uses familiar try/catch, and there's no nesting or callback pyramids. + + +**Don't forget:** async/await doesn't replace Promises. It's built on top of them. Every `async` function returns a Promise, and `await` works with any Promise. The better you understand Promises, the better you'll be at async/await. + + +--- + +## The Restaurant Analogy + +Think of async/await like ordering food at a restaurant with table service versus a fast-food counter. + +**Without async/await (callback style):** You order at the counter, then stand there awkwardly blocking everyone behind you until your food is ready. If you need multiple items, you wait for each one before ordering the next. + +**With async/await:** You sit at a table and place your order. The waiter takes it to the kitchen (starts the async operation), but you're free to chat, check your phone, or do other things (the main thread isn't blocked). When the food is ready, the waiter brings it to you (the Promise resolves) and you continue from where you left off. + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ THE RESTAURANT ANALOGY │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ async function dinner() { │ +│ │ +│ ┌──────────┐ "I'll have the ┌─────────────┐ │ +│ │ YOU │ ──────────────────────► │ KITCHEN │ │ +│ │ (code) │ pasta please" │ (server) │ │ +│ └──────────┘ await order() └─────────────┘ │ +│ │ │ │ +│ │ You're free to do │ Kitchen is │ +│ │ other things while │ preparing... │ +│ │ waiting! │ │ +│ │ │ │ +│ │ "Your pasta!" │ │ +│ ┌──────────┐ ◄────────────────────── ┌─────────────┐ │ +│ │ YOU │ Promise resolved │ KITCHEN │ │ +│ │ resume │ │ done │ │ +│ └──────────┘ └─────────────┘ │ +│ │ +│ return enjoyMeal(pasta) │ +│ } │ +│ │ +│ The KEY: You (the main thread) are NOT blocked while waiting! │ +│ Other customers (other code) can be served. │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +Here's the clever part: `await` makes your code *look* like it's waiting, but JavaScript is actually free to do other work. When the Promise resolves, your function resumes exactly where it left off. + +--- + +## The `async` Keyword + +The [`async`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function) keyword does one simple thing: **it makes a function return a Promise**. + +```javascript +// Regular function +function greet() { + return 'Hello' +} +console.log(greet()) // "Hello" + +// Async function - automatically returns a Promise +async function greetAsync() { + return 'Hello' +} +console.log(greetAsync()) // Promise {: "Hello"} +``` + +### What Happens to Return Values? + +When you return a value from an async function, it gets automatically wrapped in `Promise.resolve()`: + +```javascript +async function getValue() { + return 42 +} + +// The above is equivalent to: +function getValuePromise() { + return Promise.resolve(42) +} + +// Both work the same way: +getValue().then(value => console.log(value)) // 42 +``` + +### What Happens When You Throw? + +When you throw an error in an async function, it becomes a rejected Promise: + +```javascript +async function failingFunction() { + throw new Error('Something went wrong!') +} + +// The above is equivalent to: +function failingPromise() { + return Promise.reject(new Error('Something went wrong!')) +} + +// Both are caught the same way: +failingFunction().catch(err => console.log(err.message)) // "Something went wrong!" +``` + +### Return a Promise? No Double-Wrapping + +If you return a Promise from an async function, it doesn't get double-wrapped: + +```javascript +async function fetchData() { + // Returning a Promise directly - it's NOT double-wrapped + return fetch('/api/data') +} + +// This returns Promise, NOT Promise> +const response = await fetchData() +``` + +### Async Function Expressions and Arrow Functions + +You can use `async` with function expressions and arrow functions too: + +```javascript +// Async function expression +const fetchData = async function() { + return await fetch('/api/data') +} + +// Async arrow function +const loadData = async () => { + return await fetch('/api/data') +} + +// Async arrow function (concise body) +const getData = async () => fetch('/api/data') + +// Async method in an object +const api = { + async fetchUser(id) { + return await fetch(`/api/users/${id}`) + } +} + +// Async method in a class +class UserService { + async getUser(id) { + const response = await fetch(`/api/users/${id}`) + return response.json() + } +} +``` + + +**Common misconception:** Making a function `async` doesn't make it run in a separate thread or "in the background." JavaScript is still single-threaded. The `async` keyword simply enables the use of `await` inside the function and ensures it returns a Promise. + + +--- + +## The `await` Keyword + +The [`await`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await) keyword is where things get interesting. It **pauses the execution of an async function** until a Promise settles (fulfills or rejects), then resumes with the resolved value. + +```javascript +async function example() { + console.log('Before await') + + const result = await somePromise() // Execution pauses here + + console.log('After await:', result) // Resumes when Promise resolves +} +``` + +### Where Can You Use await? + +`await` can only be used in two places: + +1. **Inside an async function** +2. **At the top level of an ES module** (top-level await, covered later) + +```javascript +// ✓ Inside async function +async function fetchUser() { + const response = await fetch('/api/user') + return response.json() +} + +// ✓ Top-level await in ES modules +// (in a .mjs file or with "type": "module" in package.json) +const config = await fetch('/config.json').then(r => r.json()) + +// ❌ NOT in regular functions +function regularFunction() { + const data = await fetch('/api/data') // SyntaxError! +} + +// ❌ NOT in global scope of scripts (non-modules) +await fetch('/api/data') // SyntaxError in non-module scripts +``` + +### What Can You await? + +You can `await` any value, but it's most useful with Promises: + +```javascript +// Awaiting a Promise (the normal case) +const response = await fetch('/api/data') + +// Awaiting Promise.resolve() +const value = await Promise.resolve(42) +console.log(value) // 42 + +// Awaiting a non-Promise value (works, but pointless) +const num = await 42 +console.log(num) // 42 (immediately, no actual waiting) + +// Awaiting a thenable (object with .then method) +const thenable = { + then(resolve) { + setTimeout(() => resolve('thenable value'), 1000) + } +} +const result = await thenable +console.log(result) // "thenable value" (after 1 second) +``` + + +**Pro tip:** Only use `await` when you're actually waiting for a Promise. Awaiting non-Promise values works but adds unnecessary overhead and confuses anyone reading your code. + + + +**Technical detail:** Even when awaiting an already-resolved Promise or a non-Promise value, execution still pauses until the next microtask. This is why `await` always yields control back to the caller before continuing. + + +### await Pauses the Function, Not the Thread + +This trips people up. `await` pauses only the async function it's in, not the entire JavaScript thread. Other code can run while waiting: + +```javascript +async function slowOperation() { + console.log('Starting slow operation') + await new Promise(resolve => setTimeout(resolve, 2000)) + console.log('Slow operation complete') +} + +console.log('Before calling slowOperation') +slowOperation() // Starts but doesn't block +console.log('After calling slowOperation') + +// Output: +// "Before calling slowOperation" +// "Starting slow operation" +// "After calling slowOperation" +// (2 seconds later) +// "Slow operation complete" +``` + +Notice that "After calling slowOperation" prints before "Slow operation complete". The main thread wasn't blocked. + +--- + +## How await Works Under the Hood + +Let's peek under the hood at what actually happens. When you `await` a Promise, **the code after the await becomes a microtask** that runs when the Promise resolves. + +```javascript +async function example() { + console.log('1. Before await') // Runs synchronously + await Promise.resolve() + console.log('2. After await') // Runs as a microtask +} + +console.log('A. Before call') +example() +console.log('B. After call') + +// Output: +// A. Before call +// 1. Before await +// B. After call +// 2. After await +``` + +Let's trace through this step by step: + + + + `console.log('A. Before call')` executes → prints "A. Before call" + + + + The function starts executing synchronously. + `console.log('1. Before await')` executes → prints "1. Before await" + + + + `await Promise.resolve()`. The Promise is already resolved, but the code after `await` is still scheduled as a **microtask**. The function pauses and returns control to the caller. + + + + `console.log('B. After call')` executes → prints "B. After call" + + + + The event loop processes the microtask queue. The continuation of `example()` runs. + `console.log('2. After await')` executes → prints "2. After await" + + + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ await SPLITS THE FUNCTION │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ async function example() { │ +│ console.log('Before') ──────► Runs SYNCHRONOUSLY │ +│ │ +│ await somePromise() ──────► PAUSE: Schedule continuation │ +│ as microtask, return to caller │ +│ │ +│ console.log('After') ──────► Runs as MICROTASK when │ +│ } Promise resolves │ +│ │ +│ ───────────────────────────────────────────────────────────────────── │ +│ │ +│ Think of it like this - await transforms the function into: │ +│ │ +│ function example() { │ +│ console.log('Before') │ +│ return somePromise().then(() => { │ +│ console.log('After') │ +│ }) │ +│ } │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + + +This is why understanding the [Event Loop](/concepts/event-loop) is so important for async/await. The `await` keyword effectively registers a microtask, which has priority over setTimeout callbacks (macrotasks). + + +--- + +## Error Handling with try/catch + +Finally, error handling that doesn't make you want to flip a table. Instead of chaining `.catch()` after `.then()` after `.catch()`, you get to use good old try/catch blocks. + +### Basic try/catch Pattern + +```javascript +async function fetchUserData(userId) { + try { + const response = await fetch(`/api/users/${userId}`) + + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`) + } + + const user = await response.json() + return user + + } catch (error) { + console.error('Failed to fetch user:', error.message) + throw error // Re-throw if you want callers to handle it + } +} +``` + +### Catching Different Types of Errors + +```javascript +async function processOrder(orderId) { + try { + const order = await fetchOrder(orderId) + const payment = await processPayment(order) + const shipment = await createShipment(order) + return { order, payment, shipment } + + } catch (error) { + // You can check error types + if (error.name === 'NetworkError') { + console.log('Network issue - please check your connection') + } else if (error.name === 'PaymentError') { + console.log('Payment failed - please try again') + } else { + console.log('Unexpected error:', error.message) + } + throw error + } +} +``` + +### The finally Block + +The `finally` block always runs, whether the try succeeded or failed: + +```javascript +async function fetchWithLoading(url) { + showLoadingSpinner() + + try { + const response = await fetch(url) + const data = await response.json() + return data + + } catch (error) { + showErrorMessage(error.message) + throw error + + } finally { + // This ALWAYS runs - perfect for cleanup + hideLoadingSpinner() + } +} +``` + +### try/catch vs .catch() + +Both approaches work, but they have different use cases: + + + + ```javascript + // Good for: Multiple awaits where any could fail + async function getFullProfile(userId) { + try { + const user = await fetchUser(userId) + const posts = await fetchPosts(userId) + const friends = await fetchFriends(userId) + return { user, posts, friends } + } catch (error) { + // Catches any of the three failures + console.error('Profile fetch failed:', error) + return null + } + } + ``` + + + ```javascript + // Good for: Handling errors for specific operations + async function getProfileWithFallback(userId) { + const user = await fetchUser(userId) + + // Only this operation has fallback behavior + const posts = await fetchPosts(userId).catch(() => []) + + // This will still throw if it fails + const friends = await fetchFriends(userId) + + return { user, posts, friends } + } + ``` + + + +### Common Error Handling Mistake + + +**The Trap:** If you catch an error but don't re-throw it, the Promise resolves successfully (with undefined), not rejects! + + +```javascript +// ❌ WRONG - Error is swallowed, returns undefined +async function fetchData() { + try { + const response = await fetch('/api/data') + return await response.json() + } catch (error) { + console.error('Error:', error) + // Missing: throw error + } +} + +const data = await fetchData() // undefined if there was an error! + +// ✓ CORRECT - Re-throw or return a meaningful value +async function fetchData() { + try { + const response = await fetch('/api/data') + return await response.json() + } catch (error) { + console.error('Error:', error) + throw error // Re-throw to let caller handle it + // OR: return null // Return explicit fallback value + // OR: return { error: error.message } // Return error object + } +} +``` + +--- + +## Sequential vs Parallel Execution + +This is a big one. By default, `await` makes operations sequential, but often you want them to run in parallel. + +### The Problem: Unnecessary Sequential Execution + +```javascript +// ❌ SLOW - Each request waits for the previous one +async function getUserDashboard(userId) { + const user = await fetchUser(userId) // Wait ~500ms + const posts = await fetchPosts(userId) // Wait ~500ms + const notifications = await fetchNotifications(userId) // Wait ~500ms + + return { user, posts, notifications } + // Total time: ~1500ms (sequential) +} +``` + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ SEQUENTIAL EXECUTION (SLOW) │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Time: 0ms 500ms 1000ms 1500ms │ +│ │ │ │ │ │ +│ ├─────────┤ │ │ │ +│ │ user │ │ │ Total: 1500ms │ +│ │ fetch │ │ │ │ +│ └─────────┼─────────┤ │ │ +│ │ posts │ │ │ +│ │ fetch │ │ │ +│ └─────────┼─────────┤ │ +│ │ notifs │ │ +│ │ fetch │ │ +│ └─────────┘ │ +│ │ +│ Each request WAITS for the previous one to complete! │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### The Solution: Promise.all for Parallel Execution + +When operations are independent, run them in parallel: + +```javascript +// ✓ FAST - All requests run simultaneously +async function getUserDashboard(userId) { + const [user, posts, notifications] = await Promise.all([ + fetchUser(userId), // Starts immediately + fetchPosts(userId), // Starts immediately + fetchNotifications(userId) // Starts immediately + ]) + + return { user, posts, notifications } + // Total time: ~500ms (parallel - time of slowest request) +} +``` + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ PARALLEL EXECUTION (FAST) │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Time: 0ms 500ms │ +│ │ │ │ +│ ├─────────┤ │ +│ │ user │ │ +│ │ fetch │ │ +│ ├─────────┤ Total: 500ms (3x faster!) │ +│ │ posts │ │ +│ │ fetch │ │ +│ ├─────────┤ │ +│ │ notifs │ │ +│ │ fetch │ │ +│ └─────────┘ │ +│ │ +│ All requests start at the SAME TIME! │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### When to Use Sequential vs Parallel + +| Use Sequential When | Use Parallel When | +|---------------------|-------------------| +| Each operation depends on the previous result | Operations are independent | +| Order of execution matters | Order doesn't matter | +| You need to stop on first failure | All results are needed | + +### Promise.all vs Promise.allSettled + +**[Promise.all](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all)** fails fast. If any Promise rejects, the whole thing rejects. + +**[Promise.allSettled](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled)** waits for all Promises and gives you results for each (fulfilled or rejected). + +```javascript +// Promise.all - fails fast +async function getAllOrNothing() { + try { + const results = await Promise.all([ + fetchUser(1), + fetchUser(999), // This one fails + fetchUser(3) + ]) + return results + } catch (error) { + // If ANY request fails, we end up here + console.log('At least one request failed') + } +} + +// Promise.allSettled - get all results regardless of failures +async function getAllResults() { + const results = await Promise.allSettled([ + fetchUser(1), + fetchUser(999), // This one fails + fetchUser(3) + ]) + + // results = [ + // { status: 'fulfilled', value: user1 }, + // { status: 'rejected', reason: Error }, + // { status: 'fulfilled', value: user3 } + // ] + + const successful = results + .filter(r => r.status === 'fulfilled') + .map(r => r.value) + + const failed = results + .filter(r => r.status === 'rejected') + .map(r => r.reason) + + return { successful, failed } +} +``` + +### Mixed Pattern: Some Sequential, Some Parallel + +Sometimes you need a mix: some operations depend on others, but independent ones can run in parallel: + +```javascript +async function processOrder(orderId) { + // Step 1: Must fetch order first + const order = await fetchOrder(orderId) + + // Step 2: These can run in parallel (both depend on order, not each other) + const [inventory, pricing] = await Promise.all([ + checkInventory(order.items), + calculatePricing(order.items) + ]) + + // Step 3: Must wait for both before charging + const payment = await processPayment(order, pricing) + + // Step 4: These can run in parallel (both depend on payment) + const [receipt, notification] = await Promise.all([ + generateReceipt(payment), + sendConfirmationEmail(order, payment) + ]) + + return { order, payment, receipt } +} +``` + +--- + +## The 5 Most Common async/await Mistakes + +### Mistake #1: Forgetting await + +Without `await`, you get a Promise object instead of the resolved value. + +```javascript +// ❌ WRONG - response is a Promise, not a Response! +async function fetchUser() { + const response = fetch('/api/user') // Missing await! + const data = response.json() // Error: response.json is not a function + return data +} + +// ✓ CORRECT +async function fetchUser() { + const response = await fetch('/api/user') + const data = await response.json() + return data +} +``` + + +**The silent bug:** Sometimes forgetting `await` doesn't throw an error. You just get unexpected results. If you see `[object Promise]` in your output or undefined where you expected data, check for missing awaits. + + +### Mistake #2: Using await in forEach + +`forEach` and async don't play well together. It just fires and forgets: + +```javascript +// ❌ WRONG - forEach doesn't await! +async function processUsers(userIds) { + userIds.forEach(async (id) => { + const user = await fetchUser(id) + console.log(user.name) + }) + console.log('Done!') // Prints BEFORE users are fetched! +} + +// ✓ CORRECT - Use for...of for sequential +async function processUsersSequential(userIds) { + for (const id of userIds) { + const user = await fetchUser(id) + console.log(user.name) + } + console.log('Done!') // Prints after all users +} + +// ✓ CORRECT - Use Promise.all for parallel +async function processUsersParallel(userIds) { + await Promise.all( + userIds.map(async (id) => { + const user = await fetchUser(id) + console.log(user.name) + }) + ) + console.log('Done!') // Prints after all users +} +``` + +### Mistake #3: Sequential await When Parallel is Better + +We covered this above, but it's worth repeating: + +```javascript +// ❌ SLOW - 3 seconds total +async function getData() { + const a = await fetchA() // 1 second + const b = await fetchB() // 1 second + const c = await fetchC() // 1 second + return { a, b, c } +} + +// ✓ FAST - 1 second total +async function getData() { + const [a, b, c] = await Promise.all([ + fetchA(), + fetchB(), + fetchC() + ]) + return { a, b, c } +} +``` + +### Mistake #4: Not Handling Errors + +Unhandled Promise rejections can crash your application. + +```javascript +// ❌ WRONG - No error handling +async function riskyOperation() { + const data = await fetch('/api/might-fail') + return data.json() +} + +// If fetch fails, we get an unhandled rejection +riskyOperation() // No .catch(), no try/catch + +// ✓ CORRECT - Handle errors +async function safeOperation() { + try { + const data = await fetch('/api/might-fail') + return data.json() + } catch (error) { + console.error('Operation failed:', error) + return null // Or throw, or return error object + } +} + +// Or catch at the call site +riskyOperation().catch(err => console.error('Failed:', err)) +``` + +### Mistake #5: Missing await Before return in try/catch + +If you want to catch errors from a Promise inside a try/catch, you **must** use `await`. Without it, the Promise is returned before it settles, and the catch block never runs: + +```javascript +// ❌ WRONG - catch block won't catch fetch errors! +async function fetchData() { + try { + return fetch('/api/data') // Promise returned before it settles + } catch (error) { + // This NEVER runs for fetch errors! + console.error('Error:', error) + } +} + +// ✓ CORRECT - await lets catch block handle errors +async function fetchData() { + try { + return await fetch('/api/data') // await IS needed here + } catch (error) { + console.error('Error:', error) + throw error + } +} +``` + +**Why does this happen?** When you `return fetch(...)` without `await`, the Promise is immediately returned to the caller. If that Promise later rejects, the rejection happens *outside* the try/catch block, so the catch never sees it. + + +**Common misconception:** Some guides say `return await` is redundant. That's only true *outside* of try/catch blocks. Inside try/catch, you need `await` to catch errors from the Promise. + + +```javascript +// Outside try/catch, these ARE equivalent: +async function noTryCatch() { + return await fetch('/api/data') // await is optional here +} + +async function noTryCatchSimpler() { + return fetch('/api/data') // Same result, slightly cleaner +} + +// But inside try/catch, they behave DIFFERENTLY: +async function withTryCatch() { + try { + return await fetch('/api/data') // Errors ARE caught + } catch (e) { /* handles errors */ } +} + +async function brokenTryCatch() { + try { + return fetch('/api/data') // Errors NOT caught! + } catch (e) { /* never runs for fetch errors */ } +} +``` + +--- + +## async/await vs Promise Chains + +Both async/await and Promise chains achieve the same result. The choice often comes down to readability and personal preference. + +### Comparison Table + +| Aspect | async/await | Promise Chains | +|--------|-------------|----------------| +| **Readability** | Looks like sync code | Nested callbacks | +| **Error Handling** | try/catch | .catch() | +| **Debugging** | Better stack traces | Harder to trace | +| **Conditionals** | Natural if/else | Nested .then() | +| **Early Returns** | Just use return | Have to throw or nest | +| **Loops** | for/for...of work naturally | Need recursion or reduce | + +### When Promise Chains Might Be Better + +```javascript +// Promise chain is more concise for simple transformations +fetchUser(id) + .then(user => user.profileId) + .then(fetchProfile) + .then(profile => profile.avatarUrl) + +// async/await equivalent - more verbose +async function getAvatarUrl(id) { + const user = await fetchUser(id) + const profile = await fetchProfile(user.profileId) + return profile.avatarUrl +} + +// Promise.race is cleaner with raw Promises +const result = await Promise.race([ + fetch('/api/main'), + timeout(5000) +]) + +// Promise chain for "fire and forget" +saveAnalytics(data).catch(console.error) // Don't await, just catch errors +``` + +### When async/await Shines + +```javascript +// Complex conditional logic +async function processOrder(order) { + const inventory = await checkInventory(order.items) + + if (!inventory.available) { + await notifyBackorder(order) + return { status: 'backordered' } + } + + const payment = await processPayment(order) + + if (payment.requiresVerification) { + await requestVerification(payment) + return { status: 'pending_verification' } + } + + await shipOrder(order) + return { status: 'shipped' } +} + +// Loops with async operations +async function migrateUsers(users) { + for (const user of users) { + await migrateUser(user) + await delay(100) // Rate limiting + } +} + +// Complex error handling +async function robustFetch(url, retries = 3) { + for (let i = 0; i < retries; i++) { + try { + return await fetch(url) + } catch (error) { + if (i === retries - 1) throw error + await delay(1000 * (i + 1)) // Exponential backoff + } + } +} +``` + +--- + +## Top-Level await + +[Top-level await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await#top-level-await) allows you to use `await` outside of async functions. This only works in ES modules. + +```javascript +// config.js (ES module) +const response = await fetch('/config.json') +export const config = await response.json() + +// main.js +import { config } from './config.js' +console.log(config) // Config is already loaded! +``` + +### Where Top-Level await Works + +- **ES Modules** (files with `.mjs` extension or `"type": "module"` in package.json) +- **Browser ` +``` + +### Use Cases + +```javascript +// 1. Loading configuration before app starts +export const config = await loadConfig() + +// 2. Dynamic imports +const module = await import(`./locales/${language}.js`) + +// 3. Database connection +export const db = await connectToDatabase() + +// 4. Feature detection +export const supportsWebGL = await checkWebGLSupport() +``` + + +**Careful:** Top-level await blocks the loading of the module and any modules that import it. Use it sparingly, only when you truly need the value before the module can be used. + + +--- + +## Advanced Patterns + +### Retry with Exponential Backoff + +```javascript +async function fetchWithRetry(url, options = {}) { + const { retries = 3, backoff = 1000 } = options + + for (let attempt = 0; attempt < retries; attempt++) { + try { + const response = await fetch(url) + + if (!response.ok) { + throw new Error(`HTTP ${response.status}`) + } + + return response + + } catch (error) { + const isLastAttempt = attempt === retries - 1 + + if (isLastAttempt) { + throw error + } + + // Wait with exponential backoff: 1s, 2s, 4s, 8s... + const delay = backoff * Math.pow(2, attempt) + console.log(`Attempt ${attempt + 1} failed, retrying in ${delay}ms...`) + + await new Promise(resolve => setTimeout(resolve, delay)) + } + } +} + +// Usage +const response = await fetchWithRetry('/api/flaky-endpoint', { + retries: 5, + backoff: 500 +}) +``` + +### Timeout Wrapper + +```javascript +async function withTimeout(promise, ms) { + const timeout = new Promise((_, reject) => { + setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms) + }) + + return Promise.race([promise, timeout]) +} + +// Usage +try { + const response = await withTimeout(fetch('/api/slow'), 5000) + console.log('Success:', response) +} catch (error) { + console.log('Failed:', error.message) // "Timeout after 5000ms" +} +``` + +### Cancellation with AbortController + +```javascript +async function fetchWithCancellation(url, signal) { + try { + const response = await fetch(url, { signal }) + return await response.json() + } catch (error) { + if (error.name === 'AbortError') { + console.log('Fetch was cancelled') + return null + } + throw error + } +} + +// Usage +const controller = new AbortController() + +// Start the fetch +const dataPromise = fetchWithCancellation('/api/data', controller.signal) + +// Cancel after 2 seconds if not done +setTimeout(() => controller.abort(), 2000) + +const data = await dataPromise +``` + +### Async Iterators (for await...of) + +For working with streams of async data: + +```javascript +async function* generateAsyncNumbers() { + for (let i = 1; i <= 5; i++) { + await new Promise(resolve => setTimeout(resolve, 1000)) + yield i + } +} + +// Consume the async iterator +async function processNumbers() { + for await (const num of generateAsyncNumbers()) { + console.log(num) // Prints 1, 2, 3, 4, 5 (one per second) + } +} +``` + +### Converting Callback APIs to async/await + +```javascript +// Original callback-based API +function readFileCallback(path, callback) { + fs.readFile(path, 'utf8', (err, data) => { + if (err) callback(err) + else callback(null, data) + }) +} + +// Promisified version +function readFileAsync(path) { + return new Promise((resolve, reject) => { + fs.readFile(path, 'utf8', (err, data) => { + if (err) reject(err) + else resolve(data) + }) + }) +} + +// Now you can use async/await +async function processFile(path) { + const content = await readFileAsync(path) + return content.toUpperCase() +} + +// Or use util.promisify (Node.js) +const { promisify } = require('util') +const readFileAsync = promisify(fs.readFile) +``` + +--- + +## Interview Questions + +### Question 1: What's the Output? + +```javascript +async function test() { + console.log('1') + await Promise.resolve() + console.log('2') +} + +console.log('A') +test() +console.log('B') +``` + + +**Output:** `A`, `1`, `B`, `2` + +**Explanation:** +1. `console.log('A')` — synchronous → "A" +2. `test()` is called: + - `console.log('1')` — synchronous → "1" + - `await Promise.resolve()` — pauses test(), schedules continuation as microtask + - Returns to caller +3. `console.log('B')` — synchronous → "B" +4. Call stack empty → microtask runs → `console.log('2')` → "2" + +The pattern: Code before `await` runs synchronously. Code after `await` becomes a microtask. + + +### Question 2: Sequential vs Parallel + +```javascript +// Version A +async function versionA() { + const start = Date.now() + const a = await delay(1000) + const b = await delay(1000) + console.log(`Time: ${Date.now() - start}ms`) +} + +// Version B +async function versionB() { + const start = Date.now() + const [a, b] = await Promise.all([delay(1000), delay(1000)]) + console.log(`Time: ${Date.now() - start}ms`) +} + +function delay(ms) { + return new Promise(resolve => setTimeout(resolve, ms)) +} +``` + + +**versionA:** ~2000ms (sequential — waits 1s, then another 1s) + +**versionB:** ~1000ms (parallel — both delays run simultaneously) + +This is the classic "sequential vs parallel" interview question. In versionA, each `await` must complete before the next line runs. In versionB, both Promises are created immediately, then `Promise.all` waits for both to complete while they run in parallel. + + +### Question 3: Error Handling + +```javascript +async function outer() { + try { + await inner() + console.log('After inner') + } catch (e) { + console.log('Caught:', e.message) + } +} + +async function inner() { + throw new Error('Oops!') +} + +outer() +``` + + +**Output:** `Caught: Oops!` + +"After inner" is never printed because `inner()` throws, which causes the `await inner()` to reject, which jumps to the catch block. + +This demonstrates that async/await error handling works like synchronous try/catch. Errors "propagate up" naturally. + + +### Question 4: The forEach Trap + +```javascript +async function processItems() { + const items = [1, 2, 3] + + items.forEach(async (item) => { + await delay(100) + console.log(item) + }) + + console.log('Done') +} + +function delay(ms) { + return new Promise(resolve => setTimeout(resolve, ms)) +} + +processItems() +``` + + +**Output:** +``` +Done +1 +2 +3 +``` + +(Not `1`, `2`, `3`, `Done` as you might expect!) + +**Why:** `forEach` doesn't wait for async callbacks. It fires off all three async functions and immediately continues to `console.log('Done')`. The numbers print later when their delays complete. + +**Fix:** Use `for...of` for sequential or `Promise.all` with `map` for parallel. + + +### Question 5: What's Wrong Here? + +```javascript +async function getData() { + try { + return fetch('/api/data') + } catch (error) { + console.error('Failed:', error) + return null + } +} +``` + + +**Issue:** The `catch` block will never catch fetch errors. + +When you `return fetch(...)` without `await`, the Promise is returned *before* it settles. If the fetch later fails, the rejection happens outside the try/catch block. + +```javascript +// ❌ WRONG - catch never runs for fetch errors +async function getData() { + try { + return fetch('/api/data') // Promise returned immediately + } catch (error) { + console.error('Failed:', error) // Never runs! + return null + } +} + +// ✓ CORRECT - await lets catch block handle errors +async function getData() { + try { + return await fetch('/api/data') // await IS needed + } catch (error) { + console.error('Failed:', error) // Now this runs on error + return null + } +} +``` + +**Note:** Outside of try/catch, `return await` and `return` behave the same. The `await` only matters when you need to catch errors or do something with the value before returning. + + +--- + +## Key Takeaways + + +**The key things to remember:** + +1. **async/await is syntactic sugar over Promises** — it doesn't change how async works, just how you write it + +2. **async functions always return Promises** — even if you return a plain value, it's wrapped in Promise.resolve() + +3. **await pauses the function, not the thread** — other code can run while waiting; JavaScript stays non-blocking + +4. **Code after await becomes a microtask** — it runs after the current synchronous code completes, but before setTimeout callbacks + +5. **Use try/catch for error handling** — it works just like synchronous code and catches both sync errors and Promise rejections + +6. **await in forEach doesn't work as expected** — use for...of for sequential or Promise.all with map for parallel + +7. **Prefer parallel over sequential** — use Promise.all when operations are independent; it's often 2-10x faster + +8. **Don't forget await** — without it, you get a Promise object instead of the resolved value + +9. **Top-level await only works in ES modules** — not in regular scripts or CommonJS + +10. **async/await and Promises are interchangeable** — choose based on readability for your specific use case + + +--- + +## Test Your Knowledge + + + + **Answer:** + + The `async` keyword does two things: + + 1. Makes the function **always return a Promise** — even if you return a non-Promise value, it gets wrapped in `Promise.resolve()` + 2. Enables the use of `await` inside the function + + ```javascript + async function example() { + return 42 + } + + example().then(value => console.log(value)) // 42 + console.log(example()) // Promise {: 42} + ``` + + + + ```javascript + // Version A + const data = await fetchData() + + // Version B + const data = fetchData() + ``` + + **Answer:** + + - **Version A:** `data` contains the resolved value (e.g., the actual JSON object) + - **Version B:** `data` contains a Promise object, not the resolved value + + Version B is a common mistake that leads to bugs like seeing `[object Promise]` or getting undefined properties. + + + + **Answer:** + + Use `Promise.all()` to run multiple async operations simultaneously: + + ```javascript + // ❌ Sequential (slow) + const a = await fetchA() + const b = await fetchB() + const c = await fetchC() + + // ✓ Parallel (fast) + const [a, b, c] = await Promise.all([ + fetchA(), + fetchB(), + fetchC() + ]) + ``` + + For cases where you want all results even if some fail, use `Promise.allSettled()`. + + + + **Answer:** + + `forEach` is not async-aware. It doesn't wait for the callback's Promise to resolve before continuing. It just fires off all the async callbacks and moves on. + + ```javascript + // ❌ Doesn't wait + items.forEach(async item => { + await processItem(item) + }) + console.log('Done') // Prints before items are processed! + + // ✓ Sequential - use for...of + for (const item of items) { + await processItem(item) + } + console.log('Done') // Prints after all items + + // ✓ Parallel - use Promise.all with map + await Promise.all(items.map(item => processItem(item))) + console.log('Done') // Prints after all items + ``` + + + + **Answer:** + + Use `try/catch` blocks, which work just like synchronous error handling: + + ```javascript + async function fetchData() { + try { + const response = await fetch('/api/data') + if (!response.ok) { + throw new Error(`HTTP ${response.status}`) + } + return await response.json() + } catch (error) { + console.error('Fetch failed:', error) + throw error // Re-throw if caller should handle it + } finally { + // Cleanup code that always runs + } + } + ``` + + You can also use `.catch()` at the call site: `fetchData().catch(handleError)` + + + + ```javascript + console.log('1') + setTimeout(() => console.log('2'), 0) + Promise.resolve().then(() => console.log('3')) + async function test() { + console.log('4') + await Promise.resolve() + console.log('5') + } + test() + console.log('6') + ``` + + **Answer:** `1`, `4`, `6`, `3`, `5`, `2` + + **Explanation:** + 1. `'1'` — synchronous + 2. `setTimeout` callback → task queue + 3. `.then` callback → microtask queue + 4. `test()` called → `'4'` — synchronous part of async function + 5. `await` → schedules `'5'` as microtask, returns to caller + 6. `'6'` — synchronous + 7. Call stack empty → process microtasks: `'3'` then `'5'` + 8. Microtasks done → process task queue: `'2'` + + Key: Microtasks (Promises, await continuations) run before macrotasks (setTimeout). + + + +--- + +## Related Concepts + + + + async/await is built on Promises. Knowing Promises well makes async/await easier + + + Learn how JavaScript handles async operations and why await creates microtasks + + + The original async pattern that async/await replaced + + + The most common use case for async/await: making HTTP requests + + + +--- + +## Reference + + + + Complete reference for async function declarations and expressions + + + Documentation for the await operator and its behavior + + + The foundation that async/await is built on + + + Error handling syntax used with async/await + + + +## Articles + + + + The go-to reference for async/await fundamentals. Includes exercises at the end to test your understanding of rewriting promise chains. + + + Learn async patterns by building a virtual ice cream shop. The GIFs comparing sync vs async execution are worth the visit alone. + + + + Side-by-side code comparisons that show exactly how async/await cleans up promise chains. The debugging section alone is worth bookmarking. + + + Animated GIFs that show the call stack, microtask queue, and event loop in action. This is how async/await finally "clicked" for thousands of developers. + + + The pizza-and-drinks ordering example makes parallel vs sequential execution crystal clear. Essential reading once you know the basics. + + + +## Videos + + + + Web Dev Simplified breaks down async/await in 12 minutes. Perfect if you learn better from watching code being written live. + + + Wes Bos at dotJS 2017. An energetic talk that covers async/await patterns with real API calls. The crowd reactions tell you which parts trip people up. + + + Traversy Media's full async journey from callbacks through promises to async/await. Great if you want to see how we got here historically. + + + Hitesh Choudhary's hands-on walkthrough with coding examples. Hindi and English explanations make concepts accessible to a wider audience. + + diff --git a/docs/concepts/call-stack.mdx b/docs/concepts/call-stack.mdx new file mode 100644 index 00000000..0cf70308 --- /dev/null +++ b/docs/concepts/call-stack.mdx @@ -0,0 +1,998 @@ +--- +title: "Call Stack: How Function Execution Works in JavaScript" +sidebarTitle: "Call Stack: How Function Execution Works" +description: "Learn how the JavaScript call stack tracks function execution. Understand stack frames, LIFO ordering, execution contexts, stack overflow errors, and debugging with stack traces." +--- + +How does JavaScript keep track of which function is running? When a function calls another function, how does JavaScript know where to return when that function finishes? + +The answer is the **[call stack](https://developer.mozilla.org/en-US/docs/Glossary/Call_stack)**. It's JavaScript's mechanism for tracking function execution. + +```javascript +function greet(name) { + const message = createMessage(name) + console.log(message) +} + +function createMessage(name) { + return "Hello, " + name + "!" +} + +greet("Alice") // "Hello, Alice!" +``` + +When `greet` calls `createMessage`, JavaScript remembers where it was in `greet` so it can return there after `createMessage` finishes. The call stack is what makes this possible. + + +**What you'll learn in this guide:** +- What the call stack is and why JavaScript needs it +- How functions are added and removed from the stack +- What happens step-by-step when your code runs +- Why you sometimes see "Maximum call stack size exceeded" errors +- How to debug call stack issues like a pro + + + +**Prerequisite:** This guide assumes basic familiarity with [JavaScript functions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Functions). If you're new to functions, start there first! + + +--- + +## The Stack of Plates: A Real-World Analogy + +Imagine you're working in a restaurant kitchen, washing dishes. As clean plates come out, you stack them one on top of another. When a server needs a plate, they always take the one from the **top** of the stack, not from the middle or bottom. + +``` + ┌───────────┐ + │ Plate 3 │ ← You add here (top) + ├───────────┤ + │ Plate 2 │ + ├───────────┤ + │ Plate 1 │ ← First plate (bottom) + └───────────┘ +``` + +This is exactly how JavaScript keeps track of your functions! When you call a function, JavaScript puts it on top of a "stack." When that function finishes, JavaScript removes it from the top and goes back to whatever was underneath. + +This simple concept, **adding to the top and removing from the top**, is the foundation of how JavaScript executes your code. + +--- + +## What is the Call Stack? + +The **[call stack](https://developer.mozilla.org/en-US/docs/Glossary/Call_stack)** is a mechanism that JavaScript uses to keep track of where it is in your code. Think of it as JavaScript's "to-do list" for function calls, but one where it can only work on the item at the top. + +```javascript +function first() { second(); } +function second() { third(); } +function third() { console.log('Hello!'); } + +first(); +// Stack grows: [first] → [second, first] → [third, second, first] +// Stack shrinks: [second, first] → [first] → [] +``` + +### The LIFO Principle + +The call stack follows a principle called **LIFO**: **Last In, First Out**. + +- **Last In**: The most recent function call goes on top +- **First Out**: The function on top must finish before we can get to the ones below + +``` +LIFO = Last In, First Out + +┌─────────────────┐ +│ function C │ ← Last in (most recent call) +├─────────────────┤ First to finish and leave +│ function B │ +├─────────────────┤ +│ function A │ ← First in (earliest call) +└─────────────────┘ Last to finish +``` + +### Why Does JavaScript Need a Call Stack? + +JavaScript is **[single-threaded](https://developer.mozilla.org/en-US/docs/Glossary/Thread)**, meaning it can only do **one thing at a time**. The call stack helps JavaScript: + +1. **Remember where it is** — Which function is currently running? +2. **Know where to go back** — When a function finishes, where should execution continue? +3. **Keep track of local variables** — Each function has its own variables that shouldn't interfere with others + + +**ECMAScript Specification**: According to the official JavaScript specification, the call stack is implemented through "[execution contexts](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Execution_model#stack_and_execution_contexts)." Each function call creates a new execution context that gets pushed onto the stack. + + +--- + +## How the Call Stack Works: Step-by-Step + +Let's trace through a simple example to see the call stack in action. + +### A Simple Example + +```javascript +function greet(name) { + const greeting = createGreeting(name); + console.log(greeting); +} + +function createGreeting(name) { + return "Hello, " + name + "!"; +} + +// Start here +greet("Alice"); +console.log("Done!"); +``` + +### Step-by-Step Execution + + + + JavaScript begins executing your code from top to bottom. The call stack is empty. + + ``` + Call Stack: [ empty ] + ``` + + + + JavaScript sees `greet("Alice")` and pushes `greet` onto the call stack. + + ``` + Call Stack: [ greet ] + ``` + + Now JavaScript enters the `greet` function and starts executing its code. + + + + Inside `greet`, JavaScript encounters `createGreeting(name)`. It pushes `createGreeting` onto the stack. + + ``` + Call Stack: [ createGreeting, greet ] + ``` + + Notice: `greet` is **paused** while `createGreeting` runs. JavaScript can only do one thing at a time! + + + + `createGreeting` finishes and returns `"Hello, Alice!"`. JavaScript pops it off the stack. + + ``` + Call Stack: [ greet ] + ``` + + The return value (`"Hello, Alice!"`) is passed back to `greet`. + + + + Back in `greet`, the returned value is stored in `greeting`, then `console.log` runs. Finally, `greet` finishes and is popped off. + + ``` + Call Stack: [ empty ] + ``` + + + + With the stack empty, JavaScript continues to the next line: `console.log("Done!")`. + + **Output:** + ``` + Hello, Alice! + Done! + ``` + + + +### Visual Summary + + + + ``` + Step 1: Step 2: Step 3: Step 4: Step 5: + + ┌─────────┐ ┌─────────┐ ┌────────────────┐ ┌─────────┐ ┌─────────┐ + │ (empty) │ → │ greet │ → │createGreeting │ → │ greet │ → │ (empty) │ + └─────────┘ └─────────┘ ├────────────────┤ └─────────┘ └─────────┘ + │ greet │ + └────────────────┘ + + Program greet() createGreeting() createGreeting greet() + starts called called returns returns + ``` + + + | Step | Action | Stack (top → bottom) | What's Happening | + |------|--------|---------------------|------------------| + | 1 | Start | `[]` | Program begins | + | 2 | Call `greet("Alice")` | `[greet]` | Enter greet function | + | 3 | Call `createGreeting("Alice")` | `[createGreeting, greet]` | greet pauses, enter createGreeting | + | 4 | Return from createGreeting | `[greet]` | createGreeting done, back to greet | + | 5 | Return from greet | `[]` | greet done, continue program | + | 6 | `console.log("Done!")` | `[]` | Print "Done!" | + + + +--- + +## Execution Context: What's Actually on the Stack? + +When we say a function is "on the stack," what does that actually mean? Each entry on the call stack is called an **[execution context](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Execution_model#stack_and_execution_contexts)**, sometimes referred to as a **stack frame** in general computer science terms. It contains everything JavaScript needs to execute that function. + + + + The values passed to the function when it was called. + + ```javascript + function greet(name, age) { + // Arguments: { name: "Alice", age: 25 } + } + greet("Alice", 25); + ``` + + + + Variables declared inside the function with `var`, `let`, or `const`. + + ```javascript + function calculate() { + const x = 10; // Local variable + let y = 20; // Local variable + var z = 30; // Local variable + // These only exist inside this function + } + ``` + + + + The value of `this` inside the function, which depends on how the function was called. + + ```javascript + const person = { + name: "Alice", + greet() { + console.log(this.name); // 'this' refers to person + } + }; + ``` + + + + Where JavaScript should continue executing after this function returns. This is how JavaScript knows to go back to the right place in your code. + + + + Access to variables from outer (parent) functions. This is how closures work! + + ```javascript + function outer() { + const message = "Hello"; + + function inner() { + console.log(message); // Can access 'message' from outer + } + + inner(); + } + ``` + + + +### Visualizing an Execution Context + +``` +┌─────────────────────────────────────────┐ +│ EXECUTION CONTEXT │ +│ Function: greet │ +├─────────────────────────────────────────┤ +│ Arguments: { name: "Alice" } │ +│ Local Vars: { greeting: undefined } │ +│ this: window (or undefined) │ +│ Return to: line 12, main program │ +│ Outer Scope: [global scope] │ +└─────────────────────────────────────────┘ +``` + +--- + +## Nested Function Calls: A Deeper Example + +Let's look at a more complex example with multiple levels of function calls. + +```javascript +function multiply(x, y) { + return x * y; +} + +function square(n) { + return multiply(n, n); +} + +function printSquare(n) { + const result = square(n); + console.log(result); +} + +printSquare(4); +``` + +### Tracing the Execution + + + + **Step 1: Call printSquare(4)** + ``` + Stack: [ printSquare ] + ``` + + **Step 2: printSquare calls square(4)** + ``` + Stack: [ square, printSquare ] + ``` + + **Step 3: square calls multiply(4, 4)** + ``` + Stack: [ multiply, square, printSquare ] + ``` + This is the **maximum stack depth** for this program: 3 frames. + + **Step 4: multiply returns 16** + ``` + Stack: [ square, printSquare ] + ``` + + **Step 5: square returns 16** + ``` + Stack: [ printSquare ] + ``` + + **Step 6: printSquare logs 16 and returns** + ``` + Stack: [ empty ] + ``` + + **Output: `16`** + + + ``` + printSquare(4) square(4) multiply(4,4) multiply square printSquare + called called called returns 16 returns 16 returns + + ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────┐ + │printSquare │ → │ square │ → │ multiply │ → │ square │ → │printSquare │ → │ (empty) │ + └─────────────┘ ├─────────────┤ ├─────────────┤ ├─────────────┤ └─────────────┘ └─────────┘ + │printSquare │ │ square │ │printSquare │ + └─────────────┘ ├─────────────┤ └─────────────┘ + │printSquare │ + └─────────────┘ + + Depth: 1 Depth: 2 Depth: 3 Depth: 2 Depth: 1 Depth: 0 + ``` + + + + +**Understanding the flow**: Each function must completely finish before the function that called it can continue. This is why `printSquare` has to wait for `square`, and `square` has to wait for `multiply`. + + +--- + +## The #1 Call Stack Mistake: Stack Overflow + +The call stack has a **limited size**. If you keep adding functions without removing them, eventually you'll run out of space. This is called a **stack overflow**, and JavaScript throws a **[RangeError](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RangeError)** when it happens. + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ STACK OVERFLOW │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ WRONG: No Base Case RIGHT: With Base Case │ +│ ──────────────────── ───────────────────── │ +│ │ +│ function count() { function count(n) { │ +│ count() // Forever! if (n <= 0) return // Stop! │ +│ } count(n - 1) │ +│ } │ +│ │ +│ Stack grows forever... Stack grows, then shrinks │ +│ ┌─────────┐ ┌─────────┐ │ +│ │ count() │ │ count(0)│ ← Returns │ +│ ├─────────┤ ├─────────┤ │ +│ │ count() │ │ count(1)│ │ +│ ├─────────┤ ├─────────┤ │ +│ │ count() │ │ count(2)│ │ +│ ├─────────┤ └─────────┘ │ +│ │ .... │ │ +│ └─────────┘ │ +│ 💥 CRASH! ✓ Success! │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + + +**The Trap:** Recursive functions without a proper stopping condition will crash your program. The most common cause is **infinite recursion**, a function that calls itself forever without a base case. + + +### The Classic Mistake: Missing Base Case + +```javascript +// ❌ BAD: This will crash! +function countdown(n) { + console.log(n); + countdown(n - 1); // Calls itself forever! +} + +countdown(5); +``` + +**What happens:** +``` +Stack: [ countdown(5) ] +Stack: [ countdown(4), countdown(5) ] +Stack: [ countdown(3), countdown(4), countdown(5) ] +Stack: [ countdown(2), countdown(3), countdown(4), countdown(5) ] +... keeps growing forever ... +💥 CRASH: Maximum call stack size exceeded +``` + +### The Fix: Add a Base Case + +```javascript +// ✅ GOOD: This works correctly +function countdown(n) { + if (n <= 0) { + console.log("Done!"); + return; // ← BASE CASE: Stop here! + } + console.log(n); + countdown(n - 1); +} + +countdown(5); +// Output: 5, 4, 3, 2, 1, Done! +``` + +**What happens now:** +``` +Stack: [ countdown(5) ] +Stack: [ countdown(4), countdown(5) ] +Stack: [ countdown(3), countdown(4), countdown(5) ] +Stack: [ countdown(2), countdown(3), ..., countdown(5) ] +Stack: [ countdown(1), countdown(2), ..., countdown(5) ] +Stack: [ countdown(0), countdown(1), ..., countdown(5) ] + ↑ Base case reached! Start returning. +Stack: [ countdown(1), ..., countdown(5) ] +Stack: [ countdown(2), ..., countdown(5) ] +... stack unwinds ... +Stack: [ countdown(5) ] +Stack: [ empty ] +✅ Program completes successfully +``` + +### Error Messages by Browser + +| Browser | Error Message | +|---------|---------------| +| Chrome | `RangeError: Maximum call stack size exceeded` | +| Firefox | `InternalError: too much recursion` (non-standard) | +| Safari | `RangeError: Maximum call stack size exceeded` | + + +Firefox uses `InternalError` which is a non-standard error type specific to the SpiderMonkey engine. Chrome and Safari use the standard `RangeError`. + + +### Common Causes of Stack Overflow + + + + ```javascript + // Missing the stopping condition + function loop() { + loop(); + } + loop(); // 💥 Crash! + ``` + + + + ```javascript + function countUp(n) { + if (n >= 1000000000000) return; // Too far away! + countUp(n + 1); + } + countUp(0); // 💥 Crash before reaching base case + ``` + + + + ```javascript + class Person { + set name(value) { + this.name = value; // Calls the setter again! Infinite loop! + } + } + + const p = new Person(); + p.name = "Alice"; // 💥 Crash! + + // Fix: Use a different property name + class PersonFixed { + set name(value) { + this._name = value; // Use _name instead + } + } + ``` + + + + ```javascript + function a() { b(); } + function b() { a(); } // a calls b, b calls a, forever! + + a(); // 💥 Crash! + ``` + + + + +**Prevention tips:** +1. Always define a clear **base case** for recursive functions +2. Make sure each recursive call moves **toward** the base case +3. Consider using **iteration** (loops) instead of recursion for simple cases +4. Be careful with property setters, use different internal property names + + +--- + +## Debugging the Call Stack + +When something goes wrong, the call stack is your best friend for figuring out what happened. + +### Reading a Stack Trace + +When an error occurs, JavaScript gives you a **[stack trace](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/stack)**, a snapshot of the call stack at the moment of the error. + +```javascript +function a() { b(); } +function b() { c(); } +function c() { + throw new Error('Something went wrong!'); +} + +a(); +``` + +**Output:** +``` +Error: Something went wrong! + at c (script.js:4:9) + at b (script.js:2:14) + at a (script.js:1:14) + at script.js:7:1 +``` + +**How to read it:** +- Read from **top to bottom** = most recent call to oldest +- `at c (script.js:4:9)` = Error occurred in function `c`, file `script.js`, line 4, column 9 +- The trace shows you exactly how the program got to the error + +### Using Browser DevTools + + + + Press `F12` or `Cmd+Option+I` (Mac) / `Ctrl+Shift+I` (Windows) + + + + Click on the "Sources" tab (Chrome) or "Debugger" tab (Firefox) + + + + Click on a line number in your code to set a breakpoint. Execution will pause there. + + + + When paused, look at the "Call Stack" panel on the right. It shows all the functions currently on the stack. + + + + Use the step buttons to execute one line at a time and watch the stack change. + + + + +**Pro debugging tip:** If you're dealing with recursion, add a `console.log` at the start of your function to see how many times it's being called: + +```javascript +function factorial(n) { + console.log('factorial called with n =', n); + if (n <= 1) return 1; + return n * factorial(n - 1); +} +``` + + +--- + +## The Call Stack and Asynchronous Code + +You might be wondering: "If JavaScript can only do one thing at a time, how does it handle things like `setTimeout` or fetching data from a server?" + +Great question! The call stack is only **part** of the picture. + + +When you use asynchronous functions like [`setTimeout`](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout), [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API), or event listeners, JavaScript doesn't put them on the call stack immediately. Instead, they go through a different system involving the **[Event Loop](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Event_loop)** and **Task Queue**. + +This is covered in detail in the [Event Loop](/concepts/event-loop) section. + + +Here's a sneak peek: + +```javascript +console.log('First'); + +setTimeout(() => { + console.log('Second'); +}, 0); + +console.log('Third'); + +// Output: +// First +// Third +// Second ← Even with 0ms delay, this runs last! +``` + +The `setTimeout` callback doesn't go directly on the call stack. It waits in a queue until the stack is empty. This is why "Third" prints before "Second" even though the timeout is 0 milliseconds. + + + + Learn how JavaScript handles asynchronous operations + + + Modern way to handle async code + + + +--- + +## Common Misconceptions + + + + **Wrong!** The call stack and heap are completely different structures: + + | Component | Purpose | Structure | + |-----------|---------|-----------| + | **Call Stack** | Tracks function execution | Ordered (LIFO), small, fast | + | **Heap** | Stores data (objects, arrays) | Unstructured, large | + + ```javascript + function example() { + // Primitives live in the stack frame + const x = 10; + const name = "Alice"; + + // Objects live in the HEAP (reference stored in stack) + const user = { name: "Alice" }; + const numbers = [1, 2, 3]; + } + ``` + + When the function returns, the stack frame is popped (primitives gone), but heap objects persist until garbage collected. + + + + **Wrong!** When a timer finishes, the callback does NOT run immediately. It goes to the **Task Queue** and must wait for: + + 1. The call stack to be completely empty + 2. All microtasks to be processed first + 3. Its turn in the task queue + + ```javascript + console.log('Start'); + + setTimeout(() => { + console.log('Timer'); // Does NOT run at 0ms! + }, 0); + + console.log('End'); + + // Output: Start, End, Timer + // Even with 0ms delay, 'Timer' prints LAST + ``` + + The callback must wait until the current script finishes and the stack is empty. + + + + **Wrong!** JavaScript is **single-threaded**. It has ONE call stack and can only execute ONE thing at a time. + + ```javascript + function a() { + console.log('A start'); + b(); // JS pauses 'a' and runs 'b' completely + console.log('A end'); + } + + function b() { + console.log('B'); + } + + a(); + // Output: A start, B, A end (sequential, not parallel) + ``` + + **The source of confusion:** People mistake JavaScript's *asynchronous behavior* for *parallel execution*. Web APIs (timers, fetch, etc.) run in separate browser threads, but JavaScript code itself runs one operation at a time. The Event Loop coordinates callbacks, creating the *illusion* of concurrency. + + + + **Wrong!** The Promise *constructor* runs **synchronously**. Only the `.then()` callbacks are asynchronous: + + ```javascript + console.log('1'); + + new Promise((resolve) => { + console.log('2'); // Runs SYNCHRONOUSLY! + resolve(); + }).then(() => { + console.log('3'); // Async (microtask) + }); + + console.log('4'); + + // Output: 1, 2, 4, 3 + // Note: '2' prints before '4'! + ``` + + The executor function passed to `new Promise()` runs immediately on the call stack. Only the `.then()`, `.catch()`, and `.finally()` callbacks are queued as microtasks. + + + +--- + +## Key Takeaways + + +**The key things to remember about the Call Stack:** + +1. **JavaScript is single-threaded** — It has ONE call stack and can only do one thing at a time + +2. **LIFO principle** — Last In, First Out. The most recent function call finishes first + +3. **Execution contexts** — Each function call creates a "frame" containing arguments, local variables, and return address + +4. **Synchronous execution** — Functions must complete before their callers can continue + +5. **Stack overflow** — Happens when the stack gets too deep, usually from infinite recursion + +6. **Always have a base case** — Recursive functions need a stopping condition + +7. **Stack traces are your friend** — They show you exactly how your program got to an error + +8. **Async callbacks wait** — `setTimeout`, `fetch`, and event callbacks don't run until the call stack is empty + +9. **Each frame is isolated** — Local variables in one function call don't affect variables in another call of the same function + +10. **Debugging tools show the stack** — Browser DevTools let you pause execution and inspect the current call stack + + +--- + +## Test Your Knowledge + + + + **Answer:** LIFO stands for **Last In, First Out**. + + It's important because it determines the order in which functions execute and return. The most recently called function must complete before the function that called it can continue. This is how JavaScript keeps track of nested function calls and knows where to return when a function finishes. + + + + ```javascript + function a() { b(); } + function b() { c(); } + function c() { d(); } + function d() { console.log('done'); } + a(); + ``` + + **Answer:** The maximum stack depth is **4 frames**. + + ``` + Stack at deepest point: [ d, c, b, a ] + ``` + + When `d()` is executing, all four functions are on the stack. After `d()` logs "done" and returns, the stack starts unwinding. + + + + ```javascript + function greet() { + greet(); + } + greet(); + ``` + + **Answer:** This code causes a stack overflow because there's **no base case** to stop the recursion. + + - `greet()` is called + - `greet()` calls `greet()` again + - That `greet()` calls `greet()` again + - This continues forever, adding new frames to the stack + - Eventually the stack runs out of space → **Maximum call stack size exceeded** + + **Fix:** Add a condition to stop the recursion: + ```javascript + function greet(times) { + if (times <= 0) return; // Base case + console.log('Hello!'); + greet(times - 1); + } + greet(3); + ``` + + + + **Answer:** An execution context (stack frame) contains: + + 1. **Function arguments** — The values passed to the function + 2. **Local variables** — Variables declared with `var`, `let`, or `const` + 3. **The `this` value** — The context binding for the function + 4. **Return address** — Where to continue executing after the function returns + 5. **Scope chain** — Access to variables from outer (parent) functions + + This is why each function call can have its own independent set of variables without interfering with other calls. + + + + ```javascript + console.log('First') + + setTimeout(() => { + console.log('Second') + }, 0) + + console.log('Third') + ``` + + **Answer:** The output is: + ``` + First + Third + Second + ``` + + Even though `setTimeout` has a 0ms delay, "Second" prints last because: + + 1. `setTimeout` doesn't put the callback directly on the call stack + 2. Instead, the callback waits in the **task queue** + 3. The event loop only moves it to the call stack when the stack is empty + 4. "Third" runs first because it's already on the call stack + + This demonstrates that the call stack must be empty before async callbacks execute. + + + + Given this error: + ``` + Error: Something went wrong! + at c (script.js:4:9) + at b (script.js:2:14) + at a (script.js:1:14) + at script.js:7:1 + ``` + + **Answer:** Read stack traces from **top to bottom** (most recent to oldest): + + 1. **Top line** (`at c`) — Where the error actually occurred (function `c`, line 4, column 9) + 2. **Following lines** — The chain of function calls that led here + 3. **Bottom line** — Where the chain started (the initial call) + + The trace tells you: the program started at line 7, called `a()`, which called `b()`, which called `c()`, where the error was thrown. This helps you trace back through your code to find the root cause. + + + +--- + +## Related Concepts + + + + Understanding how primitives are stored in stack frames + + + Understanding variable visibility and how functions remember their environment + + + How async code works with the call stack + + + Functions that call themselves + + + +--- + +## Reference + + + + Official MDN documentation on the Call Stack + + + How the event loop interacts with the call stack + + + The error thrown when the call stack overflows + + + How to read and use stack traces for debugging + + + +## Articles + + + + The complete picture: how the Call Stack, Heap, Event Loop, and Web APIs work together. Great starting point for understanding JavaScript's runtime. + + + Beginner-friendly freeCodeCamp tutorial covering LIFO, stack traces, and stack overflow with clear code examples. + + + Go deeper into how the JS engine creates execution contexts and manages the Global Memory. Perfect for interview prep. + + + Beautiful ASCII art visualization showing step-by-step how setTimeout interacts with the Call Stack and Event Loop. + + + Advanced deep-dive into Creation vs Execution phases, Lexical Environment, and why `let`/`const` behave differently than `var`. + + + Explore the JS Engine architecture: V8, memory heap, and call stack from a systems perspective. + + + +## Courses + + + Part of the "JavaScript for Everyone" course by Mat Marquis. This free lesson explains why JavaScript is single-threaded, how the call stack manages execution contexts, and introduces the event loop and concurrency model. Beautifully written with a fun narrative style. + + +## Videos + + + + 🏆 The legendary JSConf talk that made mass developers finally "get" the event loop. Amazing visualizations — a must watch! + + + Short, sweet, and beginner-friendly. Colt Steele breaks down the call stack with practical examples. + + + Part of the popular "Namaste JavaScript" series. Akshay Saini explains execution with great visuals and examples. + + + Shows how JavaScript creates execution contexts and manages memory during function calls. Part of Codesmith's excellent "JavaScript: The Hard Parts" series. + + + Traces through nested function calls line by line, showing exactly when frames are pushed and popped. Good for visual learners who want to see each step. + + + Uses a simple factorial example to demonstrate recursion on the call stack. Under 10 minutes, perfect for a quick refresher. + + + Draws out the stack visually as code executes, making the LIFO concept easy to grasp. Includes a stack overflow example that shows what happens when things go wrong. + + + Harvard's CS50 explains call stacks from a computer science perspective — great for understanding the theory. + + + Live codes examples while explaining each concept, so you see exactly how to trace execution yourself. Great for following along in your own editor. + + + Focuses on the relationship between function invocation and stack frames. Explains why understanding the call stack helps you debug errors faster. + + diff --git a/docs/concepts/callbacks.mdx b/docs/concepts/callbacks.mdx new file mode 100644 index 00000000..bd61a787 --- /dev/null +++ b/docs/concepts/callbacks.mdx @@ -0,0 +1,1439 @@ +--- +title: "Callbacks: The Foundation of Async JavaScript" +sidebarTitle: "Callbacks: The Foundation of Async" +description: "Learn JavaScript callbacks, functions passed to other functions to be called later. Understand sync vs async callbacks, error-first patterns, callback hell, and why Promises were invented." +--- + +Why doesn't JavaScript wait? When you set a timer, make a network request, or listen for a click, how does your code keep running instead of freezing until that operation completes? + +```javascript +console.log('Before timer') + +setTimeout(function() { + console.log('Timer fired!') +}, 1000) + +console.log('After timer') + +// Output: +// Before timer +// After timer +// Timer fired! (1 second later) +``` + +The answer is **callbacks**: functions you pass to other functions, saying "call me back when you're done." Callbacks power everything async in JavaScript. Every event handler, every timer, every network request. They all rely on them. + + +**What you'll learn in this guide:** +- What callbacks are and why JavaScript uses them +- The difference between synchronous and asynchronous callbacks +- How callbacks connect to higher-order functions +- Common callback patterns (event handlers, timers, array methods) +- The error-first callback pattern (Node.js convention) +- Callback hell and the "pyramid of doom" +- How to escape callback hell +- Why Promises were invented to solve callback problems + + + +**Prerequisites:** This guide assumes familiarity with [the Event Loop](/concepts/event-loop). It's the mechanism that makes async callbacks work! You should also understand [higher-order functions](/concepts/higher-order-functions), since callbacks are passed to higher-order functions. + + +--- + +## What is a Callback? + +A **[callback](https://developer.mozilla.org/en-US/docs/Glossary/Callback_function)** is a function passed as an argument to another function, that gets called later. The other function decides when (or if) to run it. + +```javascript +// greet is a callback function +function greet(name) { + console.log(`Hello, ${name}!`) +} + +// processUserInput accepts a callback +function processUserInput(callback) { + const name = 'Alice' + callback(name) // "calling back" the function we received +} + +processUserInput(greet) // "Hello, Alice!" +``` + +The term "callback" comes from the idea of being **called back**. Think of it like getting a buzzer at a restaurant: "We'll buzz you when your table is ready." + + +**Here's the thing:** A callback is just a regular function. Nothing magical about it. What makes it a "callback" is *how it's used*: passed to another function to be executed later. + + +### Callbacks Can Be Anonymous + +You don't have to define callbacks as named functions. Anonymous functions (and arrow functions) work just as well: + +```javascript +// Named function as callback +function handleClick() { + console.log('Clicked!') +} +button.addEventListener('click', handleClick) + +// Anonymous function as callback +button.addEventListener('click', function() { + console.log('Clicked!') +}) + +// Arrow function as callback +button.addEventListener('click', () => { + console.log('Clicked!') +}) +``` + +All three do the same thing. Named functions are easier to debug though, and you can reuse them. + +--- + +## The Restaurant Buzzer Analogy + +Callbacks work like the buzzer you get at a busy restaurant: + +1. **You place an order** — You call a function and pass it a callback +2. **You get a buzzer** — The function registers your callback +3. **You go sit down** — Your code continues running (non-blocking) +4. **The buzzer goes off** — The async operation completes +5. **You pick up your food** — Your callback is executed + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ THE RESTAURANT BUZZER ANALOGY │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ YOU (Your Code) RESTAURANT (JavaScript Runtime) │ +│ │ +│ ┌──────────────┐ ┌─────────────────────────────────┐ │ +│ │ │ │ KITCHEN │ │ +│ │ "I'd like │ ────────► │ (Web APIs) │ │ +│ │ a burger" │ ORDER │ │ │ +│ │ │ │ [setTimeout: 5 min] │ │ +│ └──────────────┘ │ [fetch: waiting...] │ │ +│ │ │ [click: listening...] │ │ +│ │ └─────────────────────────────────┘ │ +│ │ │ │ +│ │ You get a buzzer │ When ready... │ +│ │ and go sit down ▼ │ +│ │ ┌─────────────────────────────────┐ │ +│ │ │ PICKUP COUNTER │ │ +│ ▼ │ (Callback Queue) │ │ +│ ┌──────────────┐ │ │ │ +│ │ │ │ [Your callback waiting here] │ │ +│ │ 📱 BUZZ! │ ◄──────── │ │ │ +│ │ │ READY! └─────────────────────────────────┘ │ +│ │ Time to │ │ +│ │ eat! │ The Event Loop calls your callback │ +│ └──────────────┘ when the kitchen (Web API) is done │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +The key insight: **you don't wait at the counter**. You give them a way to reach you (the callback), and you go do other things. That's how JavaScript stays fast. It never sits around waiting. + +```javascript +// You place your order (start async operation) +setTimeout(function eatBurger() { + console.log('Eating my burger!') // This is the callback +}, 5000) + +// You go sit down (your code continues) +console.log('Sitting down, checking my phone...') +console.log('Chatting with friends...') +console.log('Reading the menu...') + +// Output: +// Sitting down, checking my phone... +// Chatting with friends... +// Reading the menu... +// Eating my burger! (5 seconds later) +``` + +--- + +## Callbacks and Higher-Order Functions + +Callbacks and [higher-order functions](/concepts/higher-order-functions) go hand in hand: + +- A **higher-order function** is a function that accepts functions as arguments or returns them +- A **callback** is the function being passed to a higher-order function + +```javascript +// forEach is a HIGHER-ORDER FUNCTION (it accepts a function) +// The arrow function is the CALLBACK (it's being passed in) + +const numbers = [1, 2, 3] + +numbers.forEach((num) => { // ← This is the callback + console.log(num * 2) +}) +// 2, 4, 6 +``` + +Every time you use `map`, `filter`, `forEach`, `reduce`, `sort`, or `find`, you're passing callbacks to higher-order functions: + +```javascript +const users = [ + { name: 'Alice', age: 25 }, + { name: 'Bob', age: 17 }, + { name: 'Charlie', age: 30 } +] + +// filter accepts a callback that returns true/false +const adults = users.filter(user => user.age >= 18) + +// map accepts a callback that transforms each element +const names = users.map(user => user.name) + +// find accepts a callback that returns true when found +const bob = users.find(user => user.name === 'Bob') + +// sort accepts a callback that compares two elements +const byAge = users.sort((a, b) => a.age - b.age) +``` + + +**The connection:** Understanding higher-order functions helps you understand callbacks. If you're comfortable with `map` and `filter`, you already understand callbacks! The only difference with async callbacks is *when* they execute. + + +--- + +## Synchronous vs Asynchronous Callbacks + +Some callbacks run right away. Others run later. Getting this wrong will bite you. + +### Synchronous Callbacks + +**Synchronous callbacks** are executed immediately, during the function call. They block until complete. + +```javascript +const numbers = [1, 2, 3, 4, 5] + +console.log('Before map') + +const doubled = numbers.map(num => { + console.log(`Doubling ${num}`) + return num * 2 +}) + +console.log('After map') +console.log(doubled) + +// Output (all synchronous, in order): +// Before map +// Doubling 1 +// Doubling 2 +// Doubling 3 +// Doubling 4 +// Doubling 5 +// After map +// [2, 4, 6, 8, 10] +``` + +The callback runs for each element **before** `map` returns. Nothing else happens until it's done. + +**Common synchronous callbacks:** +- Array methods: `map`, `filter`, `forEach`, `reduce`, `find`, `sort`, `every`, `some` +- String methods: `replace` (with function) +- Object methods: `Object.keys().forEach()` + +### Asynchronous Callbacks + +**Asynchronous callbacks** are executed later, after the current code finishes. They don't block. + +```javascript +console.log('Before setTimeout') + +setTimeout(() => { + console.log('Inside setTimeout') +}, 0) // Even with 0ms delay! + +console.log('After setTimeout') + +// Output: +// Before setTimeout +// After setTimeout +// Inside setTimeout (runs AFTER all sync code) +``` + +Even with a 0ms delay, the callback runs **after** the synchronous code. This is because async callbacks go through the [event loop](/concepts/event-loop). + +**Common asynchronous callbacks:** +- Timers: `setTimeout`, `setInterval` +- Events: `addEventListener`, `onclick` +- Network: `XMLHttpRequest.onload`, `fetch().then()` +- Node.js I/O: `fs.readFile`, `http.get` + +### Comparison Table + +| Aspect | Synchronous Callbacks | Asynchronous Callbacks | +|--------|----------------------|------------------------| +| **When executed** | Immediately, during the function call | Later, via the event loop | +| **Blocking** | Yes — code waits for completion | No — code continues immediately | +| **Examples** | `map`, `filter`, `forEach`, `sort` | `setTimeout`, `addEventListener`, `fetch` | +| **Use case** | Data transformation, iteration | I/O, user interaction, timers | +| **Error handling** | Regular `try/catch` works | `try/catch` won't catch errors! | +| **Return value** | Can return values | Return values usually ignored | + +### The Critical Difference: Error Handling + +This trips up almost everyone: + +```javascript +// Synchronous callback - try/catch WORKS +try { + [1, 2, 3].forEach(num => { + if (num === 2) throw new Error('Found 2!') + }) +} catch (error) { + console.log('Caught:', error.message) // "Caught: Found 2!" +} + +// Asynchronous callback - try/catch DOES NOT WORK! +try { + setTimeout(() => { + throw new Error('Async error!') // This error escapes! + }, 100) +} catch (error) { + // This will NEVER run + console.log('Caught:', error.message) +} +// The error crashes your program! +``` + +Why? The `try/catch` runs immediately. By the time the async callback executes, the `try/catch` is long gone. The callback runs in a different "turn" of the event loop. + +--- + +## How Callbacks Work with the Event Loop + +To understand async callbacks, you need to see how they work with the [event loop](/concepts/event-loop). + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ ASYNC CALLBACK LIFECYCLE │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ 1. YOUR CODE RUNS │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ console.log('Start') │ │ +│ │ setTimeout(callback, 1000) // Register callback with Web API │ │ +│ │ console.log('End') │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ 2. WEB API HANDLES THE ASYNC OPERATION │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ Timer starts counting... │ │ +│ │ (Your code continues running - it doesn't wait!) │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ (after 1000ms) │ +│ 3. CALLBACK QUEUED │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ Timer done! Callback added to Task Queue │ │ +│ │ [callback] ← waiting here │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ (when call stack is empty) │ +│ 4. EVENT LOOP EXECUTES CALLBACK │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ Event Loop: "Call stack empty? Let me grab that callback..." │ │ +│ │ callback() runs! │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +Let's trace through a real example: + +```javascript +console.log('1: Script start') + +setTimeout(function first() { + console.log('2: First timeout') +}, 0) + +setTimeout(function second() { + console.log('3: Second timeout') +}, 0) + +console.log('4: Script end') +``` + +**Execution order:** + +1. `console.log('1: Script start')` — runs immediately → "1: Script start" +2. `setTimeout(first, 0)` — registers `first` callback with Web APIs +3. `setTimeout(second, 0)` — registers `second` callback with Web APIs +4. `console.log('4: Script end')` — runs immediately → "4: Script end" +5. Call stack is now empty +6. Event Loop checks Task Queue — finds `first` +7. `first()` runs → "2: First timeout" +8. Event Loop checks Task Queue — finds `second` +9. `second()` runs → "3: Second timeout" + +**Output:** +``` +1: Script start +4: Script end +2: First timeout +3: Second timeout +``` + +Even with a 0ms delay, the callbacks still run **after** all the synchronous code finishes. + + +**Read more:** Our [Event Loop guide](/concepts/event-loop) goes deep into tasks, microtasks, and rendering. If you want to understand *why* `Promise.then()` runs before `setTimeout(..., 0)`, check it out! + + +--- + +## Common Callback Patterns + +Here are the most common ways you'll see callbacks in the wild. + +### Pattern 1: Event Handlers + +The most common use of callbacks in browser JavaScript: + +```javascript +// DOM events +const button = document.getElementById('myButton') + +button.addEventListener('click', function handleClick(event) { + console.log('Button clicked!') + console.log('Event type:', event.type) // "click" + console.log('Target:', event.target) // the button element +}) + +// The callback receives an Event object with details about what happened +``` + +You can also use named functions for reusability: + +```javascript +function handleClick(event) { + console.log('Clicked:', event.target.id) +} + +function handleMouseOver(event) { + event.target.style.backgroundColor = 'yellow' +} + +button.addEventListener('click', handleClick) +button.addEventListener('mouseover', handleMouseOver) + +// Later, you can remove them: +button.removeEventListener('click', handleClick) +``` + +### Pattern 2: Timers + +`setTimeout` and `setInterval` both accept callbacks: + +```javascript +// setTimeout - runs once after delay +const timeoutId = setTimeout(function() { + console.log('This runs once after 2 seconds') +}, 2000) + +// Cancel it before it runs +clearTimeout(timeoutId) + +// setInterval - runs repeatedly +let count = 0 +const intervalId = setInterval(function() { + count++ + console.log(`Count: ${count}`) + + if (count >= 5) { + clearInterval(intervalId) // Stop after 5 times + console.log('Done!') + } +}, 1000) +``` + +**Passing arguments to timer callbacks:** + +```javascript +// Method 1: Closure (most common) +const name = 'Alice' +setTimeout(function() { + console.log(`Hello, ${name}!`) +}, 1000) + +// Method 2: setTimeout's extra arguments +setTimeout(function(greeting, name) { + console.log(`${greeting}, ${name}!`) +}, 1000, 'Hello', 'Bob') // Extra args passed to callback + +// Method 3: Arrow function with closure +const user = { name: 'Charlie' } +setTimeout(() => console.log(`Hi, ${user.name}!`), 1000) +``` + +### Pattern 3: Array Iteration + +These are synchronous callbacks, but they're everywhere: + +```javascript +const products = [ + { name: 'Laptop', price: 999, inStock: true }, + { name: 'Phone', price: 699, inStock: false }, + { name: 'Tablet', price: 499, inStock: true } +] + +// forEach - do something with each item +products.forEach(product => { + console.log(`${product.name}: $${product.price}`) +}) + +// map - transform each item into something new +const productNames = products.map(product => product.name) +// ['Laptop', 'Phone', 'Tablet'] + +// filter - keep only items that pass a test +const available = products.filter(product => product.inStock) +// [{ name: 'Laptop', ... }, { name: 'Tablet', ... }] + +// find - get the first item that passes a test +const phone = products.find(product => product.name === 'Phone') +// { name: 'Phone', price: 699, inStock: false } + +// reduce - combine all items into a single value +const totalValue = products.reduce((sum, product) => sum + product.price, 0) +// 2197 +``` + +### Pattern 4: Custom Callbacks + +You can create your own functions that accept callbacks: + +```javascript +// A function that does something and then calls you back +function fetchUserData(userId, callback) { + // Simulate async operation + setTimeout(function() { + const user = { id: userId, name: 'Alice', email: 'alice@example.com' } + callback(user) + }, 1000) +} + +// Using the function +fetchUserData(123, function(user) { + console.log('Got user:', user.name) +}) +console.log('Fetching user...') + +// Output: +// Fetching user... +// Got user: Alice (1 second later) +``` + +--- + +## The Error-First Callback Pattern + +When Node.js came along, developers needed a standard way to handle errors in async callbacks. They landed on **error-first callbacks** (also called "Node-style callbacks" or "errbacks"). + +### The Convention + +```javascript +// Error-first callback signature +function callback(error, result) { + // error: null/undefined if success, Error object if failure + // result: the data if success, usually undefined if failure +} +``` + +The first parameter is **always** reserved for an error. If the operation succeeds, `error` is `null` or `undefined`. If it fails, `error` contains an Error object. + +### Reading a File (Node.js Example) + +```javascript +const fs = require('fs') + +fs.readFile('config.json', 'utf8', function(error, data) { + // ALWAYS check for error first! + if (error) { + console.error('Failed to read file:', error.message) + return // Important: stop execution! + } + + // If we get here, error is null/undefined + console.log('File contents:', data) + const config = JSON.parse(data) + console.log('Config loaded:', config) +}) +``` + +### Why Put Error First? + +1. **Consistency** — Every callback has the same signature +2. **Can't be ignored** — The error is the first thing you see +3. **Early return** — Check for error, return early, then handle success +4. **No exceptions** — Async errors can't be caught with try/catch + +### Creating Your Own Error-First Functions + +```javascript +function divideAsync(a, b, callback) { + // Simulate async operation + setTimeout(function() { + // Check for errors + if (typeof a !== 'number' || typeof b !== 'number') { + callback(new Error('Both arguments must be numbers')) + return + } + + if (b === 0) { + callback(new Error('Cannot divide by zero')) + return + } + + // Success! Error is null, result is the value + const result = a / b + callback(null, result) + }, 100) +} + +// Using it +divideAsync(10, 2, function(error, result) { + if (error) { + console.error('Division failed:', error.message) + return + } + console.log('Result:', result) // Result: 5 +}) + +divideAsync(10, 0, function(error, result) { + if (error) { + console.error('Division failed:', error.message) // "Cannot divide by zero" + return + } + console.log('Result:', result) +}) +``` + +### Common Mistake: Forgetting to Return + +```javascript +// ❌ WRONG - code continues after error callback! +function processData(data, callback) { + if (!data) { + callback(new Error('No data provided')) + // Oops! Execution continues... + } + + // This runs even when there's an error! + const processed = transform(data) // Crash! data is undefined + callback(null, processed) +} + +// ✓ CORRECT - return after error callback +function processData(data, callback) { + if (!data) { + return callback(new Error('No data provided')) + // Or: callback(new Error(...)); return; + } + + // This only runs if data exists + const processed = transform(data) + callback(null, processed) +} +``` + + +**Always return after calling an error callback!** Otherwise, your code continues executing with invalid data. + + +--- + +## Callback Hell: The Pyramid of Doom + +When you have multiple async operations that depend on each other, callbacks nest inside callbacks. This creates the infamous "callback hell" or "pyramid of doom." + +### The Problem + +Imagine a user authentication flow: + +1. Get user from database +2. Verify password +3. Get user's profile +4. Get user's settings +5. Render the dashboard + +With callbacks, this becomes: + +```javascript +getUser(userId, function(error, user) { + if (error) { + handleError(error) + return + } + + verifyPassword(user, password, function(error, isValid) { + if (error) { + handleError(error) + return + } + + if (!isValid) { + handleError(new Error('Invalid password')) + return + } + + getProfile(user.id, function(error, profile) { + if (error) { + handleError(error) + return + } + + getSettings(user.id, function(error, settings) { + if (error) { + handleError(error) + return + } + + renderDashboard(user, profile, settings, function(error) { + if (error) { + handleError(error) + return + } + + console.log('Dashboard rendered!') + }) + }) + }) + }) +}) +``` + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ CALLBACK HELL │ +│ (The Pyramid of Doom) │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ getUser(id, function(err, user) { │ +│ verifyPassword(user, pw, function(err, valid) { │ +│ getProfile(user.id, function(err, profile) { │ +│ getSettings(user.id, function(err, settings) { │ +│ renderDashboard(user, profile, settings, function(err) { │ +│ // Finally! But look at this indentation... │ +│ }) │ +│ }) │ +│ }) │ +│ }) │ +│ }) │ +│ │ +│ Problems: │ +│ • Hard to read (horizontal scrolling) │ +│ • Hard to debug (which callback failed?) │ +│ • Hard to maintain (adding a step means more nesting) │ +│ • Error handling repeated at every level │ +│ • Variables from outer callbacks hard to track │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### Why This Hurts + +1. **Readability** — Code flows right instead of down, requiring horizontal scrolling +2. **Error handling** — Must be duplicated at every level +3. **Debugging** — Stack traces become confusing +4. **Maintenance** — Adding or removing steps is painful +5. **Variable scope** — Variables from outer callbacks are hard to track +6. **Testing** — Nearly impossible to unit test individual steps + + + +--- + +## Escaping Callback Hell + +Here's how to escape the pyramid of doom. + +### Strategy 1: Named Functions + +Extract anonymous callbacks into named functions: + +```javascript +// Before: Anonymous callback hell +getData(function(err, data) { + processData(data, function(err, processed) { + saveData(processed, function(err) { + console.log('Done!') + }) + }) +}) + +// After: Named functions +function handleData(err, data) { + if (err) return handleError(err) + processData(data, handleProcessed) +} + +function handleProcessed(err, processed) { + if (err) return handleError(err) + saveData(processed, handleSaved) +} + +function handleSaved(err) { + if (err) return handleError(err) + console.log('Done!') +} + +function handleError(err) { + console.error('Error:', err.message) +} + +// Start the chain +getData(handleData) +``` + +**Benefits:** +- Code flows vertically (easier to read) +- Functions can be reused +- Easier to debug (named functions in stack traces) +- Easier to test individually + +### Strategy 2: Early Returns and Guard Clauses + +Keep the happy path at the lowest indentation level: + +```javascript +// Instead of nested if/else +function processUser(user, callback) { + validateUser(user, function(err, isValid) { + if (err) { + callback(err) + } else { + if (isValid) { + saveUser(user, function(err, savedUser) { + if (err) { + callback(err) + } else { + callback(null, savedUser) + } + }) + } else { + callback(new Error('Invalid user')) + } + } + }) +} + +// Use early returns +function processUser(user, callback) { + validateUser(user, function(err, isValid) { + if (err) return callback(err) + if (!isValid) return callback(new Error('Invalid user')) + + saveUser(user, function(err, savedUser) { + if (err) return callback(err) + callback(null, savedUser) + }) + }) +} +``` + +### Strategy 3: Modularization + +Split your code into smaller, focused modules: + +```javascript +// auth.js +function authenticateUser(credentials, callback) { + getUser(credentials.email, function(err, user) { + if (err) return callback(err) + + verifyPassword(user, credentials.password, function(err, isValid) { + if (err) return callback(err) + if (!isValid) return callback(new Error('Invalid password')) + callback(null, user) + }) + }) +} + +// profile.js +function loadUserProfile(userId, callback) { + getProfile(userId, function(err, profile) { + if (err) return callback(err) + + getSettings(userId, function(err, settings) { + if (err) return callback(err) + callback(null, { profile, settings }) + }) + }) +} + +// main.js +authenticateUser(credentials, function(err, user) { + if (err) return handleError(err) + + loadUserProfile(user.id, function(err, data) { + if (err) return handleError(err) + renderDashboard(user, data.profile, data.settings) + }) +}) +``` + +### Strategy 4: Control Flow Libraries (Historical) + +Before Promises, libraries like [async.js](https://caolan.github.io/async/) helped manage callback flow: + +```javascript +// Using async.js waterfall (each step passes result to next) +async.waterfall([ + function(callback) { + getUser(userId, callback) + }, + function(user, callback) { + verifyPassword(user, password, function(err, isValid) { + callback(err, user, isValid) + }) + }, + function(user, isValid, callback) { + if (!isValid) return callback(new Error('Invalid password')) + getProfile(user.id, function(err, profile) { + callback(err, user, profile) + }) + }, + function(user, profile, callback) { + getSettings(user.id, function(err, settings) { + callback(err, user, profile, settings) + }) + } +], function(err, user, profile, settings) { + if (err) return handleError(err) + renderDashboard(user, profile, settings) +}) +``` + +### Strategy 5: Promises (The Modern Solution) + +[Promises](/concepts/promises) were invented specifically to solve callback hell: + +```javascript +// The same flow with Promises +getUser(userId) + .then(user => verifyPassword(user, password)) + .then(({ user, isValid }) => { + if (!isValid) throw new Error('Invalid password') + return getProfile(user.id).then(profile => ({ user, profile })) + }) + .then(({ user, profile }) => { + return getSettings(user.id).then(settings => ({ user, profile, settings })) + }) + .then(({ user, profile, settings }) => { + renderDashboard(user, profile, settings) + }) + .catch(handleError) +``` + + +This Promise chain is intentionally verbose to show how callbacks nest differently with Promises. For cleaner patterns and best practices, check out our [Promises guide](/concepts/promises). + + +Or with [async/await](/concepts/async-await): + +```javascript +// The same flow with async/await +async function initDashboard(userId, password) { + try { + const user = await getUser(userId) + const isValid = await verifyPassword(user, password) + + if (!isValid) throw new Error('Invalid password') + + const profile = await getProfile(user.id) + const settings = await getSettings(user.id) + + renderDashboard(user, profile, settings) + } catch (error) { + handleError(error) + } +} +``` + + +**Promises and async/await are built on callbacks.** They don't replace callbacks. They provide a cleaner abstraction over them. Under the hood, Promise `.then()` handlers are still callbacks! + + +--- + +## Common Callback Mistakes + +### Mistake 1: Calling a Callback Multiple Times + +A callback should typically be called exactly once, either with an error or with a result: + +```javascript +// ❌ WRONG - callback called multiple times! +function fetchData(url, callback) { + fetch(url) + .then(response => { + callback(null, response) // Called on success + }) + .catch(error => { + callback(error) // Called on error + }) + .finally(() => { + callback(null, 'done') // Called ALWAYS, even after success or error! + }) +} + +// ✓ CORRECT - callback called exactly once +function fetchData(url, callback) { + fetch(url) + .then(response => callback(null, response)) + .catch(error => callback(error)) +} +``` + +### Mistake 2: Synchronous and Asynchronous Mixing (Zalgo) + +A function should be consistently sync or async, never both. This inconsistency is nicknamed "releasing Zalgo," a reference to an internet meme about unleashing chaos. And chaos is exactly what you get when code behaves unpredictably: + +```javascript +// ❌ WRONG - sometimes sync, sometimes async (Zalgo!) +function getData(cache, callback) { + if (cache.has('data')) { + callback(null, cache.get('data')) // Sync! + return + } + + fetchFromServer(function(err, data) { + callback(err, data) // Async! + }) +} + +// This causes unpredictable behavior: +let value = 'initial' +getData(cache, function(err, data) { + value = data +}) +console.log(value) // "initial" or the data? Depends on cache! + +// ✓ CORRECT - always async +function getData(cache, callback) { + if (cache.has('data')) { + // Use setTimeout to make it async (works in browsers and Node.js) + setTimeout(function() { + callback(null, cache.get('data')) + }, 0) + return + } + + fetchFromServer(function(err, data) { + callback(err, data) + }) +} +``` + +### Mistake 3: Losing `this` Context + +Regular functions lose their `this` binding when used as callbacks: + +```javascript +// ❌ WRONG - this is undefined/global +const user = { + name: 'Alice', + greetLater: function() { + setTimeout(function() { + console.log(`Hello, ${this.name}!`) // this.name is undefined! + }, 1000) + } +} +user.greetLater() // "Hello, undefined!" + +// ✓ CORRECT - Use arrow function (inherits this) +const user = { + name: 'Alice', + greetLater: function() { + setTimeout(() => { + console.log(`Hello, ${this.name}!`) // Arrow function keeps this + }, 1000) + } +} +user.greetLater() // "Hello, Alice!" + +// ✓ CORRECT - Use bind +const user = { + name: 'Alice', + greetLater: function() { + setTimeout(function() { + console.log(`Hello, ${this.name}!`) + }.bind(this), 1000) // Explicitly bind this + } +} +user.greetLater() // "Hello, Alice!" + +// ✓ CORRECT - Save reference to this +const user = { + name: 'Alice', + greetLater: function() { + const self = this // Save reference + setTimeout(function() { + console.log(`Hello, ${self.name}!`) + }, 1000) + } +} +user.greetLater() // "Hello, Alice!" +``` + +### Mistake 4: Not Handling Errors + +Always handle errors in async callbacks. Unhandled errors can crash your application: + +```javascript +// ❌ WRONG - error ignored +fs.readFile('config.json', function(err, data) { + const config = JSON.parse(data) // Crashes if err exists! + startApp(config) +}) + +// ✓ CORRECT - error handled +fs.readFile('config.json', function(err, data) { + if (err) { + console.error('Could not read config:', err.message) + process.exit(1) + return + } + + try { + const config = JSON.parse(data) + startApp(config) + } catch (parseError) { + console.error('Invalid JSON in config:', parseError.message) + process.exit(1) + } +}) +``` + +--- + +## Historical Context: Why JavaScript Uses Callbacks + +Understanding *why* JavaScript uses callbacks helps everything click into place. + +### The Birth of JavaScript (1995) + +JavaScript was created by Brendan Eich at Netscape in just 10 days. Its primary purpose was to make web pages interactive, responding to user clicks, form submissions, and other events. + +### The Single-Threaded Design + +JavaScript was designed to be **single-threaded**: one thing at a time. Why? + +1. **Simplicity** — No race conditions, deadlocks, or complex synchronization +2. **DOM Safety** — Multiple threads modifying the DOM would cause chaos +3. **Browser Reality** — Early browsers couldn't handle multi-threaded scripts + +But single-threaded means a problem: **you can't block waiting for things.** + +If JavaScript waited for a network request to complete, the entire page would freeze. Users couldn't click, scroll, or do anything. That's unacceptable for a UI language. + +### The Callback Solution + +Callbacks solved this problem neatly: + +1. **Register interest** — "When this happens, call this function" +2. **Continue immediately** — Don't block, keep the UI responsive +3. **React later** — When the event occurs, the callback runs + +```javascript +// This pattern was there from day one +element.onclick = function() { + alert('Clicked!') +} + +// The page doesn't freeze waiting for a click +// JavaScript registers the callback and moves on +// When clicked, the callback runs +``` + +### The Evolution + +| Year | Development | +|------|-------------| +| 1995 | JavaScript created with event callbacks | +| 1999 | XMLHttpRequest (AJAX) — async HTTP with callbacks | +| 2009 | Node.js — callbacks for server-side I/O | +| 2012 | Callback hell becomes a recognized problem | +| 2015 | ES6 Promises — official solution to callback hell | +| 2017 | ES8 async/await — syntactic sugar for Promises | + +### Callbacks Are Still The Foundation + +Even with Promises and async/await, callbacks are everywhere: + +- **Event handlers** still use callbacks +- **Array methods** still use callbacks +- **Promises** use callbacks internally (`.then(callback)`) +- **async/await** is syntactic sugar over Promise callbacks + +Callbacks aren't obsolete. They're the foundation that everything else builds upon. + +--- + +## Key Takeaways + + +**The key things to remember:** + +1. **A callback is a function passed to another function** to be executed later — nothing magical + +2. **Callbacks can be synchronous or asynchronous** — array methods are sync, timers and events are async + +3. **Higher-order functions and callbacks are two sides of the same coin** — one accepts, one is passed + +4. **Async callbacks go through the event loop** — they never run until all sync code finishes + +5. **Error-first callbacks: `callback(error, result)`** — always check error first, return after handling + +6. **You can't use try/catch for async callbacks** — the catch is gone by the time the callback runs + +7. **Callback hell is real** — deeply nested callbacks become unreadable and unmaintainable + +8. **Escape callback hell with:** named functions, modularization, early returns, or Promises + +9. **Promises were invented to solve callback problems** — but they still use callbacks under the hood + +10. **Callbacks are the foundation** — events, Promises, async/await all build on callbacks + + +--- + +## Test Your Knowledge + + + + **Answer:** + + **Synchronous callbacks** execute immediately, during the function call. They block until complete. Examples: `map`, `filter`, `forEach`. + + ```javascript + [1, 2, 3].forEach(n => console.log(n)) // Runs immediately, blocks + console.log('Done') // Runs after forEach completes + ``` + + **Asynchronous callbacks** execute later, via the event loop. They don't block. Examples: `setTimeout`, `addEventListener`, `fs.readFile`. + + ```javascript + setTimeout(() => console.log('Timer'), 0) // Registers, doesn't block + console.log('Done') // Runs BEFORE the timer callback + ``` + + + + **Answer:** + + The error-first convention exists because: + + 1. **Consistency** — Every async callback has the same signature: `(error, result)` + 2. **Can't be ignored** — The error is the first thing you must deal with + 3. **Forces handling** — You naturally check for errors before using results + 4. **No exceptions** — Async errors can't be caught with try/catch, so they must be passed + + ```javascript + fs.readFile('file.txt', (error, data) => { + if (error) { + // Handle error FIRST + console.error(error) + return + } + // Safe to use data + console.log(data) + }) + ``` + + + + ```javascript + console.log('A') + + setTimeout(() => console.log('B'), 0) + + console.log('C') + + setTimeout(() => console.log('D'), 0) + + console.log('E') + ``` + + **Answer:** `A`, `C`, `E`, `B`, `D` + + **Explanation:** + 1. `console.log('A')` — sync, runs immediately → "A" + 2. `setTimeout(..., 0)` — registers callback B, continues + 3. `console.log('C')` — sync, runs immediately → "C" + 4. `setTimeout(..., 0)` — registers callback D, continues + 5. `console.log('E')` — sync, runs immediately → "E" + 6. Call stack empty → event loop runs callback B → "B" + 7. Event loop runs callback D → "D" + + Even with 0ms delay, setTimeout callbacks run after all sync code. + + + + **Answer:** Three common approaches: + + **1. Arrow functions** (recommended — they inherit `this` from enclosing scope): + ```javascript + const obj = { + name: 'Alice', + greet() { + setTimeout(() => { + console.log(this.name) // "Alice" + }, 100) + } + } + ``` + + **2. Using `bind()`**: + ```javascript + setTimeout(function() { + console.log(this.name) + }.bind(this), 100) + ``` + + **3. Saving a reference**: + ```javascript + const self = this + setTimeout(function() { + console.log(self.name) + }, 100) + ``` + + + + **Answer:** + + The `try/catch` block executes **synchronously**. By the time an async callback runs, the try/catch is long gone. It's on a different "turn" of the event loop. + + ```javascript + try { + setTimeout(() => { + throw new Error('Async error!') // This escapes! + }, 100) + } catch (e) { + // This NEVER catches the error + console.log('Caught:', e) + } + + // The error crashes the program because: + // 1. try/catch runs immediately + // 2. setTimeout registers callback and returns + // 3. try/catch completes (nothing thrown yet!) + // 4. 100ms later, callback runs and throws + // 5. No try/catch exists at that point + ``` + + This is why we use error-first callbacks or Promise `.catch()` for async error handling. + + + + **Answer:** + + **1. Named functions** — Extract callbacks into named functions: + ```javascript + function handleUser(err, user) { + if (err) return handleError(err) + getProfile(user.id, handleProfile) + } + getUser(userId, handleUser) + ``` + + **2. Modularization** — Split into separate modules/functions: + ```javascript + // auth.js exports authenticateUser() + // profile.js exports loadProfile() + // main.js composes them + ``` + + **3. Promises/async-await** — Use modern async patterns: + ```javascript + const user = await getUser(userId) + const profile = await getProfile(user.id) + ``` + + Other approaches: control flow libraries (async.js), early returns, keeping nesting shallow. + + + +--- + +## Related Concepts + + + + How JavaScript schedules and executes async callbacks + + + Modern solution to callback hell + + + Cleaner syntax for Promise-based async code + + + Functions that accept or return other functions + + + +--- + +## Reference + + + + Official MDN glossary definition of callback functions + + + Documentation for the setTimeout timer function + + + How to register event callbacks on DOM elements + + + Synchronous callback pattern with array iteration + + + +## Articles + + + + Starts with the "I'll call you back" phone analogy that makes callbacks click. Builds up from simple examples to async patterns step by step. + + + Uses a script-loading example to show why callbacks exist and how they solve real problems. The "pyramid of doom" section shows exactly how callback hell develops. + + + + +## Videos + + + + Mosh uses a movie database example that shows callbacks in a realistic context. Great production quality with on-screen code highlighting. + + + Kyle explains callbacks in under 8 minutes with zero fluff. Perfect if you want a quick refresher without sitting through a long tutorial. + + + MPJ's signature style makes this feel like a conversation, not a lecture. Explains the "why" behind callbacks, not just the "how." + + + Covers the full async story: callbacks, then Promises, then async/await. Watch this one if you want to see how each pattern improves on the last. + + + Walks through callbacks with a setTimeout example, then shows how to create your own callback-accepting functions. Good for hands-on learners. + + diff --git a/docs/concepts/clean-code.mdx b/docs/concepts/clean-code.mdx new file mode 100644 index 00000000..45f5d336 --- /dev/null +++ b/docs/concepts/clean-code.mdx @@ -0,0 +1,900 @@ +--- +title: "Clean Code: Writing Readable JavaScript" +sidebarTitle: "Clean Code: Writing Readable JavaScript" +description: "Learn clean code principles for JavaScript. Covers meaningful naming, small functions, DRY, avoiding side effects, and best practices to write maintainable code." +--- + +Why do some codebases feel like a maze while others read like a well-written story? What makes code easy to change versus code that makes you want to rewrite everything from scratch? + +```javascript +// Which would you rather debug at 2am? + +// Version A +function p(a, b) { + let x = 0 + for (let i = 0; i < a.length; i++) { + if (a[i].s === 1) x += a[i].p * b + } + return x +} + +// Version B +function calculateActiveProductsTotal(products, taxRate) { + let total = 0 + for (const product of products) { + if (product.status === PRODUCT_STATUS.ACTIVE) { + total += product.price * taxRate + } + } + return total +} +``` + +**Clean code** is code that's easy to read, easy to understand, and easy to change. The principles behind clean code were popularized by Robert C. Martin's book *[Clean Code: A Handbook of Agile Software Craftsmanship](https://www.amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882)*, and Ryan McDermott adapted these principles specifically for JavaScript in his [clean-code-javascript](https://github.com/ryanmcdermott/clean-code-javascript) repository (94k+ GitHub stars). Both are essential reading for any JavaScript developer. + + +**What you'll learn in this guide:** +- What makes code "clean" and why it matters +- Naming conventions that make code self-documenting +- How to write small, focused functions that do one thing +- The DRY principle and when to apply it +- How to avoid side effects and write predictable code +- Using early returns to reduce nesting +- When to write comments (and when not to) +- SOLID principles applied to JavaScript + + +--- + +## The Newspaper Analogy + +Think of your code like a newspaper article. A reader should understand the gist from the headline, get more details from the first paragraph, and find supporting information as they read further. Your code should work the same way: high-level functions at the top, implementation details below. + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ CODE LIKE A NEWSPAPER │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ // HEADLINE: What does this module do? │ +│ export function processUserOrder(userId, orderId) { │ +│ const user = getUser(userId) │ +│ const order = getOrder(orderId) │ +│ validateOrder(user, order) │ +│ return chargeAndShip(user, order) │ +│ } │ +│ │ +│ // DETAILS: How does it do it? │ +│ function getUser(userId) { ... } │ +│ function getOrder(orderId) { ... } │ +│ function validateOrder(user, order) { ... } │ +│ function chargeAndShip(user, order) { ... } │ +│ │ +│ Read top-to-bottom. The "what" comes before the "how". │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Meaningful Naming + +Names are everywhere in code: variables, functions, classes, files. Good names make comments unnecessary. Bad names make simple code confusing. + +### Use Pronounceable, Searchable Names + +```javascript +// ❌ What does this even mean? +const yyyymmdstr = moment().format('YYYY/MM/DD') +const d = new Date() +const t = d.getTime() + +// ✓ Crystal clear +const currentDate = moment().format('YYYY/MM/DD') +const now = new Date() +const timestamp = now.getTime() +``` + +### Use the Same Word for the Same Concept + +Pick one word per concept and stick with it. If you fetch users with `getUser()`, don't also have `fetchClient()` and `retrieveCustomer()`. + +```javascript +// ❌ Inconsistent - which one do I use? +getUserInfo() +fetchClientData() +retrieveCustomerRecord() + +// ✓ Consistent vocabulary +getUser() +getClient() +getCustomer() +``` + +### Avoid Mental Mapping + +Single-letter variables force readers to remember what `a`, `x`, or `l` mean. Be explicit. + +```javascript +// ❌ What is 'l'? A number? A location? A letter? +locations.forEach(l => { + doStuff() + // ... 50 lines later + dispatch(l) // Wait, what was 'l' again? +}) + +// ✓ No guessing required +locations.forEach(location => { + doStuff() + dispatch(location) +}) +``` + +### Don't Add Unnecessary Context + +If your class is called `Car`, you don't need `carMake`, `carModel`, `carColor`. The context is already there. + +```javascript +// ❌ Redundant prefixes +const Car = { + carMake: 'Honda', + carModel: 'Accord', + carColor: 'Blue' +} + +// ✓ Context is already clear +const Car = { + make: 'Honda', + model: 'Accord', + color: 'Blue' +} +``` + +--- + +## Functions Should Do One Thing + +This is the single most important rule in clean code. When functions do one thing, they're easier to name, easier to test, and easier to reuse. + +### Keep Functions Small and Focused + +```javascript +// ❌ This function does too many things +function emailClients(clients) { + clients.forEach(client => { + const clientRecord = database.lookup(client) + if (clientRecord.isActive()) { + email(client) + } + }) +} + +// ✓ Each function has one job +function emailActiveClients(clients) { + clients + .filter(isActiveClient) + .forEach(email) +} + +function isActiveClient(client) { + const clientRecord = database.lookup(client) + return clientRecord.isActive() +} +``` + +### Limit Function Parameters + +Two or fewer parameters is ideal. If you need more, use an object with destructuring. This also makes the call site self-documenting. + +```javascript +// ❌ What do these arguments mean? +createMenu('Settings', 'User preferences', 'Save', true) + +// ✓ Self-documenting with destructuring +createMenu({ + title: 'Settings', + body: 'User preferences', + buttonText: 'Save', + cancellable: true +}) + +function createMenu({ title, body, buttonText, cancellable = false }) { + // ... +} +``` + +### Don't Use Boolean Flags + +A boolean parameter is a sign that the function does more than one thing. Split it into two functions instead. + +```javascript +// ❌ Boolean flag = function does two things +function createFile(name, isTemp) { + if (isTemp) { + fs.create(`./temp/${name}`) + } else { + fs.create(name) + } +} + +// ✓ Two focused functions +function createFile(name) { + fs.create(name) +} + +function createTempFile(name) { + createFile(`./temp/${name}`) +} +``` + +--- + +## Avoid Magic Numbers and Strings + +Magic values are unexplained numbers or strings scattered through your code. They make code hard to understand and hard to change. + +```javascript +// ❌ What is 86400000? Why 18? +setTimeout(blastOff, 86400000) + +if (user.age > 18) { + allowAccess() +} + +if (status === 1) { + // ... +} + +// ✓ Named constants are searchable and self-documenting +const MILLISECONDS_PER_DAY = 60 * 60 * 24 * 1000 +const MINIMUM_LEGAL_AGE = 18 +const STATUS = { + ACTIVE: 1, + INACTIVE: 0 +} + +setTimeout(blastOff, MILLISECONDS_PER_DAY) + +if (user.age > MINIMUM_LEGAL_AGE) { + allowAccess() +} + +if (status === STATUS.ACTIVE) { + // ... +} +``` + + +**Pro tip:** ESLint's `no-magic-numbers` rule can automatically flag magic numbers in your code. + + +--- + +## DRY: Don't Repeat Yourself + +Duplicate code means multiple places to update when logic changes. But be careful: a bad abstraction is worse than duplication. Only abstract when you see a clear pattern. + +```javascript +// ❌ Duplicate logic +function showDeveloperList(developers) { + developers.forEach(dev => { + const salary = dev.calculateSalary() + const experience = dev.getExperience() + const githubLink = dev.getGithubLink() + render({ salary, experience, githubLink }) + }) +} + +function showManagerList(managers) { + managers.forEach(mgr => { + const salary = mgr.calculateSalary() + const experience = mgr.getExperience() + const portfolio = mgr.getPortfolio() + render({ salary, experience, portfolio }) + }) +} + +// ✓ Unified with type-specific handling +function showEmployeeList(employees) { + employees.forEach(employee => { + const baseData = { + salary: employee.calculateSalary(), + experience: employee.getExperience() + } + + const extraData = employee.type === 'developer' + ? { githubLink: employee.getGithubLink() } + : { portfolio: employee.getPortfolio() } + + render({ ...baseData, ...extraData }) + }) +} +``` + +--- + +## Avoid Side Effects + +A function has a side effect when it does something other than take inputs and return outputs: modifying a global variable, writing to a file, or mutating an input parameter. Side effects make code unpredictable and hard to test. For a deeper dive, see our [Pure Functions](/concepts/pure-functions) guide. + +```javascript +// ❌ Mutates the original array - side effect! +function addItemToCart(cart, item) { + cart.push({ item, date: Date.now() }) +} + +// ✓ Returns a new array - no side effects +function addItemToCart(cart, item) { + return [...cart, { item, date: Date.now() }] +} +``` + +```javascript +// ❌ Modifies global state +let name = 'Ryan McDermott' + +function splitName() { + name = name.split(' ') // Mutates global! +} + +// ✓ Pure function - no globals modified +function splitName(name) { + return name.split(' ') +} + +const fullName = 'Ryan McDermott' +const nameParts = splitName(fullName) +``` + +--- + +## Early Returns and Guard Clauses + +Deeply nested code is hard to follow. Use early returns to handle edge cases first, then write the main logic without extra indentation. + +```javascript +// ❌ Deeply nested - hard to follow +function getPayAmount(employee) { + let result + if (employee.isSeparated) { + result = { amount: 0, reason: 'separated' } + } else { + if (employee.isRetired) { + result = { amount: 0, reason: 'retired' } + } else { + // ... complex salary calculation + result = { amount: salary, reason: 'employed' } + } + } + return result +} + +// ✓ Guard clauses - flat and readable +function getPayAmount(employee) { + if (employee.isSeparated) { + return { amount: 0, reason: 'separated' } + } + + if (employee.isRetired) { + return { amount: 0, reason: 'retired' } + } + + // Main logic at the top level + const salary = calculateSalary(employee) + return { amount: salary, reason: 'employed' } +} +``` + +The same applies to loops. Use `continue` to skip iterations instead of nesting: + +```javascript +// ❌ Unnecessary nesting +for (const user of users) { + if (user.isActive) { + if (user.hasPermission) { + processUser(user) + } + } +} + +// ✓ Flat and scannable +for (const user of users) { + if (!user.isActive) continue + if (!user.hasPermission) continue + + processUser(user) +} +``` + +--- + +## Comments: Less is More + +Good code mostly documents itself. Comments should explain *why*, not *what*. If you need a comment to explain what code does, consider rewriting the code to be clearer. + +### Don't State the Obvious + +```javascript +// ❌ These comments add nothing +function hashIt(data) { + // The hash + let hash = 0 + + // Length of string + const length = data.length + + // Loop through every character + for (let i = 0; i < length; i++) { + // Get character code + const char = data.charCodeAt(i) + // Make the hash + hash = (hash << 5) - hash + char + // Convert to 32-bit integer + hash &= hash + } + return hash +} + +// ✓ Only comment what's not obvious +function hashIt(data) { + let hash = 0 + const length = data.length + + for (let i = 0; i < length; i++) { + const char = data.charCodeAt(i) + hash = (hash << 5) - hash + char + hash &= hash // Convert to 32-bit integer + } + return hash +} +``` + +### Don't Leave Commented-Out Code + +That's what version control is for. Delete it. If you need it later, check the git history. + +```javascript +// ❌ Dead code cluttering the file +doStuff() +// doOtherStuff() +// doSomeMoreStuff() +// doSoMuchStuff() + +// ✓ Clean +doStuff() +``` + +### Don't Write Journal Comments + +Git log exists for a reason. + +```javascript +// ❌ This is what git history is for +/** + * 2016-12-20: Removed monads (RM) + * 2016-10-01: Added special monads (JP) + * 2016-02-03: Removed type-checking (LI) + */ +function combine(a, b) { + return a + b +} + +// ✓ Just the code +function combine(a, b) { + return a + b +} +``` + +--- + +## SOLID Principles in JavaScript + +SOLID is a set of five principles that help you write maintainable, flexible code. Here's how they apply to JavaScript: + + + + A class or module should have only one reason to change. + + ```javascript + // ❌ UserSettings handles both settings AND authentication + class UserSettings { + constructor(user) { + this.user = user + } + + changeSettings(settings) { + if (this.verifyCredentials()) { + // update settings + } + } + + verifyCredentials() { + // authentication logic + } + } + + // ✓ Separate responsibilities + class UserAuth { + constructor(user) { + this.user = user + } + + verifyCredentials() { + // authentication logic + } + } + + class UserSettings { + constructor(user, auth) { + this.user = user + this.auth = auth + } + + changeSettings(settings) { + if (this.auth.verifyCredentials()) { + // update settings + } + } + } + ``` + + + + Code should be open for extension but closed for modification. Add new features by adding new code, not changing existing code. + + ```javascript + // ❌ Must modify this function for every new shape + function getArea(shape) { + if (shape.type === 'circle') { + return Math.PI * shape.radius ** 2 + } else if (shape.type === 'rectangle') { + return shape.width * shape.height + } + // Add another if for every new shape... + } + + // ✓ Extend by adding new classes + class Shape { + getArea() { + throw new Error('getArea must be implemented') + } + } + + class Circle extends Shape { + constructor(radius) { + super() + this.radius = radius + } + + getArea() { + return Math.PI * this.radius ** 2 + } + } + + class Rectangle extends Shape { + constructor(width, height) { + super() + this.width = width + this.height = height + } + + getArea() { + return this.width * this.height + } + } + ``` + + + + Child classes should be usable wherever parent classes are expected without breaking the code. + + ```javascript + // ❌ Square breaks when used where Rectangle is expected + class Rectangle { + constructor() { + this.width = 0 + this.height = 0 + } + + setWidth(width) { + this.width = width + } + + setHeight(height) { + this.height = height + } + + getArea() { + return this.width * this.height + } + } + + class Square extends Rectangle { + setWidth(width) { + this.width = width + this.height = width // Breaks LSP! + } + + setHeight(height) { + this.width = height + this.height = height + } + } + + // This fails for Square - expects 20, gets 25 + function calculateAreas(rectangles) { + rectangles.forEach(rect => { + rect.setWidth(4) + rect.setHeight(5) + console.log(rect.getArea()) // Square returns 25, not 20! + }) + } + + // ✓ Better: separate classes, no inheritance relationship + class Rectangle { + constructor(width, height) { + this.width = width + this.height = height + } + + getArea() { + return this.width * this.height + } + } + + class Square { + constructor(side) { + this.side = side + } + + getArea() { + return this.side * this.side + } + } + ``` + + + + Don't force clients to depend on methods they don't use. In JavaScript, use optional configuration objects instead of requiring many parameters. + + ```javascript + // ❌ Forcing clients to provide options they don't need + class DOMTraverser { + constructor(settings) { + this.settings = settings + this.rootNode = settings.rootNode + this.settings.animationModule.setup() // Required even if not needed! + } + } + + const traverser = new DOMTraverser({ + rootNode: document.body, + animationModule: { setup() {} } // Must provide even if not animating + }) + + // ✓ Make features optional + class DOMTraverser { + constructor(settings) { + this.settings = settings + this.rootNode = settings.rootNode + + if (settings.animationModule) { + settings.animationModule.setup() + } + } + } + + const traverser = new DOMTraverser({ + rootNode: document.body + // animationModule is optional now + }) + ``` + + + + Depend on abstractions, not concrete implementations. Inject dependencies rather than instantiating them inside your classes. + + ```javascript + // ❌ Tightly coupled to InventoryRequester + class InventoryTracker { + constructor(items) { + this.items = items + this.requester = new InventoryRequester() // Hard dependency + } + } + + // ✓ Dependency injection + class InventoryTracker { + constructor(items, requester) { + this.items = items + this.requester = requester // Injected - can be any requester + } + } + ``` + + + +--- + +## Write Testable Code + +Functions that do one thing with no side effects are easy to test. If a function is hard to test, it's often a sign that it's doing too much or has hidden dependencies. Clean code and testable code go hand in hand. + +--- + +## Key Takeaways + + +**The key things to remember:** + +1. **Names matter** — Use meaningful, pronounceable, searchable names. Good names eliminate the need for comments. + +2. **Functions should do one thing** — This is the most important rule. Small, focused functions are easier to name, test, and reuse. + +3. **Limit function parameters** — Two or fewer is ideal. Use object destructuring for more. + +4. **Eliminate magic numbers** — Use named constants that explain what values mean. + +5. **DRY, but don't over-abstract** — Remove duplication, but a bad abstraction is worse than duplication. + +6. **Avoid side effects** — Prefer pure functions that don't mutate inputs or global state. + +7. **Use early returns** — Guard clauses reduce nesting and make code easier to follow. + +8. **Comments explain why, not what** — If you need to explain what code does, rewrite the code. + +9. **Delete dead code** — Commented-out code and unused functions clutter your codebase. Git remembers. + +10. **Use tools** — ESLint catches issues, Prettier handles formatting. Don't argue about style. + + +--- + +## Test Your Knowledge + + + + ```javascript + function process(data) { + // ... + } + ``` + + **Answer:** + + The name `process` is too vague. It doesn't tell you what kind of processing happens or what kind of data is expected. Better names would be `validateUserInput`, `parseJsonResponse`, or `calculateOrderTotal`, depending on what the function actually does. + + + + ```javascript + function createUser(name, email, age, isAdmin, sendWelcomeEmail) { + // ... + } + ``` + + **Answer:** + + Too many parameters (5). It's hard to remember the order, and the boolean flags (`isAdmin`, `sendWelcomeEmail`) suggest the function might be doing multiple things. Refactor to use an options object: + + ```javascript + function createUser({ name, email, age, isAdmin = false }) { + // ... + } + + function sendWelcomeEmail(user) { + // Separate function for separate concern + } + ``` + + + + **Answer:** + + Write comments when you need to explain *why* something is done a certain way, especially for: + - Business logic that isn't obvious from the code + - Workarounds for bugs or edge cases + - Legal or licensing requirements + - Complex algorithms where the approach isn't self-evident + + Don't write comments that explain *what* the code does. If the code needs explanation, rewrite it to be clearer. + + + + **Answer:** + + A magic number is an unexplained numeric literal in code, like `86400000` or `18`. They're bad because: + - You can't search for what they mean + - They don't explain their purpose + - If the value needs to change, you have to find every occurrence + + Replace with named constants: `MILLISECONDS_PER_DAY` or `MINIMUM_LEGAL_AGE`. + + + + ```javascript + function processUser(user) { + if (user) { + if (user.isActive) { + if (user.hasPermission) { + return doSomething(user) + } + } + } + return null + } + ``` + + **Answer:** + + Use guard clauses (early returns) to flatten the nesting: + + ```javascript + function processUser(user) { + if (!user) return null + if (!user.isActive) return null + if (!user.hasPermission) return null + + return doSomething(user) + } + ``` + + Each guard clause handles one edge case, and the main logic sits at the top level without indentation. + + + +--- + +## Related Concepts + + + + Deep dive into functions without side effects and why they make code predictable + + + ES6+ features like destructuring and arrow functions that enable cleaner code + + + How to handle errors cleanly without swallowing exceptions or cluttering code + + + Reusable solutions that embody clean code principles at a higher level + + + +--- + +## Books + + + The foundational text by Robert C. Martin that started the clean code movement. While examples are in Java, the principles apply to any language. A must-read for every developer. + + +## Articles + + + + The definitive JavaScript adaptation of Clean Code principles with 94k+ GitHub stars. Every example is practical and immediately applicable to your code. + + + freeCodeCamp's beginner-friendly introduction covering the "why" behind each clean code principle. Great starting point if you're new to these concepts. + + + javascript.info's practical guide to syntax, formatting, and style. Includes a visual cheat sheet you can reference while coding. + + + A satirical guide showing what NOT to do. The humor makes the anti-patterns memorable, and you'll recognize some of these mistakes in real codebases. + + + +## Videos + + + + Fireship's fast-paced video showing modern patterns that replace outdated approaches. Great examples of before/after refactoring. + + + freeCodeCamp's multi-part series covering each clean code principle in depth with live coding. Perfect for visual learners. + + + Robert C. Martin himself explaining clean code fundamentals. Hearing it from the source gives you the philosophy behind the principles. + + diff --git a/docs/concepts/currying-composition.mdx b/docs/concepts/currying-composition.mdx new file mode 100644 index 00000000..a226f1d6 --- /dev/null +++ b/docs/concepts/currying-composition.mdx @@ -0,0 +1,1351 @@ +--- +title: "Currying & Composition: Functional Patterns in JavaScript" +sidebarTitle: "Currying & Composition: Functional Patterns" +description: "Learn currying and function composition in JavaScript. Build reusable functions from simple pieces using curry, compose, and pipe for cleaner, modular code." +--- + +How does `add(1)(2)(3)` even work? Why do libraries like [Lodash](https://lodash.com/) and [Ramda](https://ramdajs.com/) let you call functions in multiple ways? And what if you could build complex data transformations by snapping together tiny, single-purpose functions like LEGO blocks? + +```javascript +// Currying: one argument at a time +const add = a => b => c => a + b + c +add(1)(2)(3) // 6 + +// Composition: chain functions together +const process = pipe( + getName, + trim, + capitalize +) +process({ name: " alice " }) // "Alice" +``` + +These two techniques, **currying** and **function composition**, are core to functional programming. They let you write smaller, more reusable functions and combine them into powerful pipelines. Once you understand them, you'll see opportunities to simplify your code everywhere. + + +**What you'll learn in this guide:** +- What currying is and how `add(1)(2)(3)` actually works +- The difference between currying and partial application (they're not the same!) +- How to implement your own `curry()` helper function +- What function composition is and why it matters +- How to build `compose()` and `pipe()` from scratch +- Why currying and composition work so well together +- When to use libraries like Lodash vs vanilla JavaScript +- Real-world patterns used in production codebases + + + +**Prerequisites:** This guide assumes you understand [closures](/concepts/scope-and-closures) and [higher-order functions](/concepts/higher-order-functions). Currying depends entirely on closures to work, and both currying and composition involve functions that return functions. + + +--- + +## What is Currying? + +**Currying** is a transformation that converts a function with multiple arguments into a sequence of functions, each taking a single argument. It's named after mathematician Haskell Curry. + +Instead of calling `add(1, 2, 3)` with all arguments at once, a curried version lets you call `add(1)(2)(3)`, providing one argument at a time. Each call returns a new function waiting for the next argument. + +```javascript +// Regular function: takes all arguments at once +function add(a, b, c) { + return a + b + c +} +add(1, 2, 3) // 6 + +// Curried function: takes one argument at a time +function curriedAdd(a) { + return function(b) { + return function(c) { + return a + b + c + } + } +} +curriedAdd(1)(2)(3) // 6 +``` + +With arrow functions, curried functions become beautifully concise: + +```javascript +const add = a => b => c => a + b + c +add(1)(2)(3) // 6 +``` + + +**Key insight:** Currying doesn't call the function. It transforms it. The original logic only runs when ALL arguments have been provided. + + +--- + +## The Pizza Restaurant Analogy + +Imagine you're at a build-your-own pizza restaurant. Instead of shouting your entire order at once ("Large thin-crust pepperoni pizza!"), you go through a series of stations: + +1. **Size station:** "What size?" → "Large" → You get a ticket for a large pizza +2. **Crust station:** "What crust?" → "Thin" → Your ticket now says large thin-crust +3. **Toppings station:** "What toppings?" → "Pepperoni" → Your pizza is made! + +Each station remembers your previous choices and waits for just one more piece of information. + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ THE PIZZA RESTAURANT ANALOGY │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ orderPizza(size)(crust)(toppings) │ +│ │ +│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ +│ │ SIZE STATION │ │ CRUST STATION │ │TOPPING STATION│ │ +│ │ │ │ │ │ │ │ +│ │ "What size?" │ ──► │ "What crust?" │ ──► │ "Toppings?" │ ──► 🍕 │ +│ │ "Large" │ │ "Thin" │ │ "Pepperoni" │ │ +│ │ │ │ │ │ │ │ +│ └───────────────┘ └───────────────┘ └───────────────┘ │ +│ │ │ │ │ +│ ▼ ▼ ▼ │ +│ Returns function Returns function Returns the │ +│ that remembers that remembers final pizza! │ +│ size="Large" size + crust │ +│ │ +│ Each station REMEMBERS your previous choices using closures! │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +Here's that pizza order in code: + +```javascript +const orderPizza = size => crust => topping => { + return `${size} ${crust}-crust ${topping} pizza` +} + +// Full order at once +orderPizza("Large")("Thin")("Pepperoni") +// "Large Thin-crust Pepperoni pizza" + +// Or step by step +const largeOrder = orderPizza("Large") // Remembers size +const largeThinOrder = largeOrder("Thin") // Remembers size + crust +const myPizza = largeThinOrder("Pepperoni") // Final pizza! +// "Large Thin-crust Pepperoni pizza" + +// Create reusable "order templates" +const orderLarge = orderPizza("Large") +const orderLargeThin = orderLarge("Thin") + +orderLargeThin("Mushroom") // "Large Thin-crust Mushroom pizza" +orderLargeThin("Hawaiian") // "Large Thin-crust Hawaiian pizza" +``` + +The magic is that each intermediate function "remembers" the arguments from previous calls. That's [closures](/concepts/scope-and-closures) at work! + +--- + +## How Currying Works Step by Step + +Let's trace through exactly what happens when you call a curried function: + +```javascript +const add = a => b => c => a + b + c + +// Step 1: Call add(1) +const step1 = add(1) +// Returns: b => c => 1 + b + c +// The value 1 is "closed over" - remembered by the returned function + +// Step 2: Call step1(2) +const step2 = step1(2) +// Returns: c => 1 + 2 + c +// Now both 1 and 2 are remembered + +// Step 3: Call step2(3) +const result = step2(3) +// Returns: 1 + 2 + 3 = 6 +// All arguments collected, computation happens! + +console.log(result) // 6 +``` + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ HOW CURRYING EXECUTES │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ add(1)(2)(3) │ +│ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ add(1) │ │ +│ │ a = 1 │ │ +│ │ Returns: b => c => 1 + b + c │ │ +│ └──────────────────────────────┬──────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ (2) ← called on returned function │ │ +│ │ b = 2, a = 1 (from closure) │ │ +│ │ Returns: c => 1 + 2 + c │ │ +│ └──────────────────────────────┬──────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ (3) ← called on returned function │ │ +│ │ c = 3, b = 2, a = 1 (all from closures) │ │ +│ │ Returns: 1 + 2 + 3 = 6 │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### The Closure Connection + +Currying depends entirely on [closures](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures) to work. Each nested function "closes over" the arguments from its parent function, keeping them alive even after the parent returns. + +```javascript +const multiply = a => b => a * b + +const double = multiply(2) // 'a' is now 2, locked in by closure +const triple = multiply(3) // Different closure, 'a' is 3 + +double(5) // 10 (2 * 5) +triple(5) // 15 (3 * 5) +double(10) // 20 (2 * 10) + +// 'double' and 'triple' each have their own closure +// with their own remembered value of 'a' +``` + +--- + +## Implementing a Curry Helper + +Writing curried functions manually works, but it's tedious for functions with many parameters. Let's build a `curry()` helper that transforms any function automatically. + +### Basic Curry (Two Arguments) + +```javascript +function curry(fn) { + return function(a) { + return function(b) { + return fn(a, b) + } + } +} + +// Usage +const add = (a, b) => a + b +const curriedAdd = curry(add) + +curriedAdd(1)(2) // 3 +``` + +### Advanced Curry (Any Number of Arguments) + +This version handles functions with any number of arguments and supports calling with multiple arguments at once. It uses [`fn.length`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/length) to know how many arguments the original function expects: + +```javascript +function curry(fn) { + return function curried(...args) { + // If we have enough arguments, call the original function + if (args.length >= fn.length) { + return fn.apply(this, args) + } + + // Otherwise, return a function that collects more arguments + return function(...nextArgs) { + return curried.apply(this, args.concat(nextArgs)) + } + } +} +``` + +Let's break down how this works: + +```javascript +function sum(a, b, c) { + return a + b + c +} + +const curriedSum = curry(sum) + +// All these work: +curriedSum(1, 2, 3) // 6 - called normally +curriedSum(1)(2)(3) // 6 - fully curried +curriedSum(1, 2)(3) // 6 - mixed +curriedSum(1)(2, 3) // 6 - mixed +``` + + +```javascript +// Initial call: curry(sum) +// fn = sum, fn.length = 3 +// Returns the 'curried' function + +// Call: curriedSum(1) +// args = [1], args.length (1) < fn.length (3) +// Returns a new function that remembers [1] + +// Call: (previousResult)(2) +// args = [1, 2], args.length (2) < fn.length (3) +// Returns a new function that remembers [1, 2] + +// Call: (previousResult)(3) +// args = [1, 2, 3], args.length (3) >= fn.length (3) +// Calls sum(1, 2, 3) and returns 6 +``` + + +### ES6 Concise Version + +For those who love one-liners: + +```javascript +const curry = fn => + function curried(...args) { + return args.length >= fn.length + ? fn(...args) + : (...next) => curried(...args, ...next) + } +``` + + +**Limitation:** The `fn.length` property doesn't count [rest parameters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters) or parameters with [default values](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Default_parameters): + +```javascript +function withRest(...args) {} // length is 0 +function withDefault(a, b = 2) {} // length is 1 + +// Curry won't work correctly with these! +// You'd need to specify arity manually: +// curry(fn, expectedArgCount) +``` + + +--- + +## Currying vs Partial Application + +These terms are often confused, but they're different techniques: + +| Aspect | Currying | Partial Application | +|--------|----------|---------------------| +| Arguments per call | Always **one** | Any number | +| What it returns | Chain of unary functions | Single function with some args fixed | +| Transformation | Structural (changes function shape) | Creates specialized version | + +### Currying Example + +Currying always produces functions that take exactly one argument: + +```javascript +// Curried: each call takes ONE argument +const add = a => b => c => a + b + c + +add(1) // Returns: b => c => 1 + b + c +add(1)(2) // Returns: c => 1 + 2 + c +add(1)(2)(3) // Returns: 6 +``` + +### Partial Application Example + +Partial application fixes some arguments upfront, and the resulting function takes all remaining arguments at once: + +```javascript +// Partial application helper +function partial(fn, ...presetArgs) { + return function(...laterArgs) { + return fn(...presetArgs, ...laterArgs) + } +} + +function greet(greeting, punctuation, name) { + return `${greeting}, ${name}${punctuation}` +} + +// Fix the first two arguments +const greetExcitedly = partial(greet, "Hello", "!") + +greetExcitedly("Alice") // "Hello, Alice!" +greetExcitedly("Bob") // "Hello, Bob!" + +// The returned function takes remaining args TOGETHER, not one at a time +``` + +### Visual Comparison + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ CURRYING VS PARTIAL APPLICATION │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Original: greet(greeting, punctuation, name) │ +│ │ +│ CURRYING: │ +│ ───────── │ +│ curriedGreet("Hello")("!")("Alice") │ +│ │ │ │ │ +│ ▼ ▼ ▼ │ +│ [1 arg] → [1 arg] → [1 arg] → result │ +│ │ +│ PARTIAL APPLICATION: │ +│ ──────────────────── │ +│ partial(greet, "Hello", "!")("Alice") │ +│ │ │ │ +│ ▼ ▼ │ +│ [2 args fixed] → [1 arg] → result │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + + +**When to use which?** +- **Currying:** When you want maximum flexibility in how arguments are provided +- **Partial Application:** When you want to create specialized versions of functions with some arguments preset + + +--- + +## Real-World Currying Patterns + +Currying isn't just a theoretical concept. Here are patterns you'll see in production code: + +### 1. Configurable Logging + +```javascript +// Curried logger factory +const createLogger = level => timestamp => message => { + const time = timestamp ? new Date().toISOString() : '' + console.log(`[${level}]${time ? ' ' + time : ''} ${message}`) +} + +// Create specialized loggers +const info = createLogger('INFO')(true) +const debug = createLogger('DEBUG')(true) +const error = createLogger('ERROR')(true) + +// Use them +info('Application started') // [INFO] 2024-01-15T10:30:00.000Z Application started +debug('Processing request') // [DEBUG] 2024-01-15T10:30:00.000Z Processing request +error('Connection failed') // [ERROR] 2024-01-15T10:30:00.000Z Connection failed + +// Logger without timestamp for development +const quickLog = createLogger('LOG')(false) +quickLog('Quick debug message') // [LOG] Quick debug message +``` + +### 2. API Client Factory + +```javascript +const createApiClient = baseUrl => endpoint => options => { + return fetch(`${baseUrl}${endpoint}`, options) + .then(res => res.json()) +} + +// Create clients for different APIs +const githubApi = createApiClient('https://api.github.com') +const myApi = createApiClient('https://api.myapp.com') + +// Create endpoint-specific fetchers +const getGithubUser = githubApi('/users') +const getMyAppUsers = myApi('/users') + +// Use them +getGithubUser({ method: 'GET' }) + .then(users => console.log(users)) +``` + +### 3. Event Handler Configuration + +```javascript +const handleEvent = eventType => element => callback => { + element.addEventListener(eventType, callback) + + // Return cleanup function + return () => element.removeEventListener(eventType, callback) +} + +// Create specialized handlers +const onClick = handleEvent('click') +const onHover = handleEvent('mouseenter') + +// Attach to elements +const button = document.querySelector('#myButton') +const removeClick = onClick(button)(() => console.log('Clicked!')) + +// Later: cleanup +removeClick() +``` + +### 4. Validation Functions + +```javascript +const isGreaterThan = min => value => value > min +const isLessThan = max => value => value < max +const hasLength = length => str => str.length === length + +// Create specific validators +const isAdult = isGreaterThan(17) +const isValidAge = isLessThan(120) +const isValidZipCode = hasLength(5) + +// Use with array methods +const ages = [15, 22, 45, 8, 67] +const adults = ages.filter(isAdult) // [22, 45, 67] + +const zipCodes = ['12345', '1234', '123456', '54321'] +const validZips = zipCodes.filter(isValidZipCode) // ['12345', '54321'] +``` + +### 5. Discount Calculator + +```javascript +const applyDiscount = discountPercent => price => { + return price * (1 - discountPercent / 100) +} + +const tenPercentOff = applyDiscount(10) +const twentyPercentOff = applyDiscount(20) +const blackFridayDeal = applyDiscount(50) + +tenPercentOff(100) // 90 +twentyPercentOff(100) // 80 +blackFridayDeal(100) // 50 + +// Apply to multiple items +const prices = [100, 200, 50, 75] +const discountedPrices = prices.map(tenPercentOff) // [90, 180, 45, 67.5] +``` + +--- + +## What is Function Composition? + +**Function composition** is the process of combining two or more functions to produce a new function. The output of one function becomes the input of the next. + +In mathematics, composition is written as `(f ∘ g)(x) = f(g(x))`. In code, we read this as "f after g" or "first apply g, then apply f to the result." + +```javascript +// Individual functions +const add10 = x => x + 10 +const multiply2 = x => x * 2 +const subtract5 = x => x - 5 + +// Manual composition (nested calls) +const result = subtract5(multiply2(add10(5))) +// Step by step: 5 → 15 → 30 → 25 + +// With a compose function +const composed = compose(subtract5, multiply2, add10) +composed(5) // 25 +``` + +Why compose instead of nesting? Because this: + +```javascript +addGreeting(capitalize(trim(getName(user)))) +``` + +Becomes this: + +```javascript +const processUser = compose( + addGreeting, + capitalize, + trim, + getName +) +processUser(user) +``` + +Much easier to read, modify, and test! + +--- + +## The Assembly Line Analogy + +Think of function composition like a factory assembly line. Raw materials enter one end, pass through a series of stations, and a finished product comes out the other end. + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ THE ASSEMBLY LINE ANALOGY │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ RAW INPUT ──► [Station A] ──► [Station B] ──► [Station C] ──► OUTPUT │ +│ │ +│ pipe(stationA, stationB, stationC)(rawInput) │ +│ │ +│ ───────────────────────────────────────────────────────────────────── │ +│ │ +│ Example: Transform user data │ +│ │ +│ { name: " ALICE " } │ +│ │ │ +│ ▼ │ +│ ┌─────────────┐ │ +│ │ getName │ → " ALICE " │ +│ └─────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────┐ │ +│ │ trim │ → "ALICE" │ +│ └─────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────┐ │ +│ │ toLowerCase │ → "alice" │ +│ └─────────────┘ │ +│ │ │ +│ ▼ │ +│ Final output: "alice" │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +Each station: +1. Takes input from the previous station +2. Does ONE specific transformation +3. Passes output to the next station + +This is exactly how composed functions work! + +--- + +## compose() and pipe() + +There are two ways to compose functions, differing only in direction: + +| Function | Direction | Reads like... | +|----------|-----------|---------------| +| `compose(f, g, h)` | Right to left | Math: `f(g(h(x)))` | +| `pipe(f, g, h)` | Left to right | A recipe: "first f, then g, then h" | + +### Implementing pipe() + +`pipe` flows left-to-right, which many developers find more intuitive. It uses [`reduce()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce) to chain functions together: + +```javascript +const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x) +``` + +Let's trace through it: + +```javascript +const getName = obj => obj.name +const toUpperCase = str => str.toUpperCase() +const addExclaim = str => str + '!' + +const shout = pipe(getName, toUpperCase, addExclaim) + +shout({ name: 'alice' }) + +// reduce trace: +// Initial: x = { name: 'alice' } +// Step 1: getName({ name: 'alice' }) → 'alice' +// Step 2: toUpperCase('alice') → 'ALICE' +// Step 3: addExclaim('ALICE') → 'ALICE!' +// Result: 'ALICE!' +``` + +### Implementing compose() + +`compose` flows right-to-left, matching mathematical notation. It uses [`reduceRight()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduceRight) instead: + +```javascript +const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x) +``` + +```javascript +// compose processes right-to-left +const shout = compose(addExclaim, toUpperCase, getName) +shout({ name: 'alice' }) // 'ALICE!' + +// This is equivalent to: +addExclaim(toUpperCase(getName({ name: 'alice' }))) +``` + +### Which Should You Use? + +```javascript +// These produce the same result: +pipe(a, b, c)(x) // a first, then b, then c +compose(c, b, a)(x) // Same! c(b(a(x))) +``` + +Most developers prefer `pipe` because: +1. It reads left-to-right like English +2. Functions are listed in execution order +3. It's easier to follow the data flow + +```javascript +// pipe: reads in order of execution +const processUser = pipe( + validateInput, // First + sanitizeData, // Second + saveToDatabase, // Third + sendNotification // Fourth +) + +// compose: reads in reverse order +const processUser = compose( + sendNotification, // Fourth (but listed first) + saveToDatabase, // Third + sanitizeData, // Second + validateInput // First (but listed last) +) +``` + +--- + +## Building Data Pipelines + +Composition really shines when building data transformation pipelines: + +```javascript +const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x) + +// Individual transformation functions +const removeSpaces = str => str.trim() +const toLowerCase = str => str.toLowerCase() +const splitWords = str => str.split(' ') +const capitalizeFirst = words => words.map((w, i) => + i === 0 ? w : w[0].toUpperCase() + w.slice(1) +) +const joinWords = words => words.join('') + +// Compose them into a pipeline +const toCamelCase = pipe( + removeSpaces, + toLowerCase, + splitWords, + capitalizeFirst, + joinWords +) + +toCamelCase(' HELLO WORLD ') // 'helloWorld' +toCamelCase('my variable name') // 'myVariableName' +``` + +### Real-World Pipeline: Processing API Data + +```javascript +// Transform API response into display format +const processApiResponse = pipe( + // Extract data from response + response => response.data, + + // Filter active users only + users => users.filter(u => u.isActive), + + // Sort by name + users => users.sort((a, b) => a.name.localeCompare(b.name)), + + // Transform to display format + users => users.map(u => ({ + id: u.id, + displayName: `${u.firstName} ${u.lastName}`, + email: u.email + })), + + // Take first 10 + users => users.slice(0, 10) +) + +// Use it +fetch('/api/users') + .then(res => res.json()) + .then(processApiResponse) + .then(users => renderUserList(users)) +``` + +--- + +## Why Currying and Composition Work Together + +Currying and composition are natural partners. Here's why: + +### The Problem: Functions with Multiple Arguments + +Composition works best with functions that take a single argument and return a single value. But many useful functions need multiple arguments: + +```javascript +const add = (a, b) => a + b +const multiply = (a, b) => a * b + +// This doesn't work! +const addThenMultiply = pipe(add, multiply) +addThenMultiply(1, 2) // NaN - multiply receives one value, not two +``` + +### The Solution: Currying + +Currying converts multi-argument functions into chains of single-argument functions, making them perfect for composition: + +```javascript +// Curried versions +const add = a => b => a + b +const multiply = a => b => a * b + +// Now we can compose! +const add5 = add(5) // x => 5 + x +const double = multiply(2) // x => 2 * x + +const add5ThenDouble = pipe(add5, double) +add5ThenDouble(10) // (10 + 5) * 2 = 30 +``` + +### Data-Last Parameter Order + +For composition to work smoothly, the **data** should be the **last** parameter. This is called "data-last" design: + +```javascript +// ❌ Data-first (hard to compose) +const map = (array, fn) => array.map(fn) +const filter = (array, fn) => array.filter(fn) + +// ✓ Data-last (easy to compose) +const map = fn => array => array.map(fn) +const filter = fn => array => array.filter(fn) + +// Now they compose beautifully +const double = x => x * 2 +const isEven = x => x % 2 === 0 + +const doubleEvens = pipe( + filter(isEven), + map(double) +) + +doubleEvens([1, 2, 3, 4, 5, 6]) // [4, 8, 12] +``` + +### Point-Free Style + +When currying and composition combine, you can write code without explicitly mentioning the data being processed. This is called **point-free** style: + +```javascript +// With explicit data parameter (pointed style) +const processNumbers = numbers => { + return numbers + .filter(x => x > 0) + .map(x => x * 2) + .reduce((sum, x) => sum + x, 0) +} + +// Point-free style (no explicit 'numbers' parameter) +const isPositive = x => x > 0 +const double = x => x * 2 +const sum = (a, b) => a + b + +const processNumbers = pipe( + filter(isPositive), + map(double), + reduce(sum, 0) +) + +// Both do the same thing: +processNumbers([1, -2, 3, -4, 5]) // 18 +``` + +Point-free code focuses on the transformations, not the data. It's often more declarative and easier to reason about. + +--- + +## Lodash, Ramda, and Vanilla JavaScript + +Libraries like [Lodash](https://lodash.com/) and [Ramda](https://ramdajs.com/) are popular because they provide battle-tested implementations of currying, composition, and many other utilities. + +### Why Use a Library? + +Libraries offer features our simple implementations lack: + +```javascript +import _ from 'lodash' + +// 1. Placeholder support +const greet = _.curry((greeting, name) => `${greeting}, ${name}!`) +greet(_.__, 'Alice')('Hello') // "Hello, Alice!" +// The __ placeholder lets you skip arguments + +// 2. Works with variadic functions +const sum = _.curry((...nums) => nums.reduce((a, b) => a + b, 0), 3) +sum(1)(2)(3) // 6 + +// 3. Auto-curried utility functions +_.map(x => x * 2)([1, 2, 3]) // [2, 4, 6] +// Lodash/fp provides auto-curried, data-last versions +``` + +### Ramda: Built for Composition + +[Ramda](https://ramdajs.com/) is designed from the ground up for functional programming: + +```javascript +import * as R from 'ramda' + +// All functions are auto-curried +R.add(1)(2) // 3 +R.add(1, 2) // 3 + +// Data-last by default +R.map(x => x * 2, [1, 2, 3]) // [2, 4, 6] +R.map(x => x * 2)([1, 2, 3]) // [2, 4, 6] + +// Built-in compose and pipe +const processUser = R.pipe( + R.prop('name'), + R.trim, + R.toLower +) + +processUser({ name: ' ALICE ' }) // 'alice' +``` + +### Lodash/fp: Functional Lodash + +Lodash provides a functional programming variant: + +```javascript +import fp from 'lodash/fp' + +// Auto-curried, data-last +const getAdultNames = fp.pipe( + fp.filter(user => user.age >= 18), + fp.map(fp.get('name')), + fp.sortBy(fp.identity) +) + +const users = [ + { name: 'Charlie', age: 25 }, + { name: 'Alice', age: 17 }, + { name: 'Bob', age: 30 } +] + +getAdultNames(users) // ['Bob', 'Charlie'] +``` + +### Vanilla JavaScript Alternatives + +You don't always need a library. Here are vanilla implementations for common patterns: + +```javascript +// Curry +const curry = fn => { + return function curried(...args) { + return args.length >= fn.length + ? fn(...args) + : (...next) => curried(...args, ...next) + } +} + +// Pipe and Compose +const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x) +const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x) + +// Partial Application +const partial = (fn, ...presetArgs) => (...laterArgs) => fn(...presetArgs, ...laterArgs) + +// Data-last map and filter +const map = fn => arr => arr.map(fn) +const filter = fn => arr => arr.filter(fn) +const reduce = (fn, initial) => arr => arr.reduce(fn, initial) +``` + +### When to Use What? + +| Situation | Recommendation | +|-----------|----------------| +| Learning/small project | Vanilla JS implementations | +| Already using Lodash | Use `lodash/fp` for functional code | +| Heavy functional programming | Consider Ramda | +| Bundle size matters | Vanilla JS or tree-shakeable imports | +| Team familiarity | Match existing codebase patterns | + +--- + +## Common Currying Mistakes + +### Mistake #1: Forgetting Curried Functions Return Functions + +```javascript +const add = a => b => a + b + +// ❌ Wrong: Forgot the second call +const result = add(1) +console.log(result) // [Function] - not a number! + +// ✓ Correct +const result = add(1)(2) +console.log(result) // 3 +``` + +### Mistake #2: Wrong Argument Order + +For composition to work, data should come last: + +```javascript +// ❌ Data-first: hard to compose +const multiply = (value, factor) => value * factor + +// ✓ Data-last: composes well +const multiply = factor => value => value * factor + +const double = multiply(2) +const triple = multiply(3) + +pipe(double, triple)(5) // 30 +``` + +### Mistake #3: Currying Functions with Rest Parameters + +```javascript +function sum(...nums) { + return nums.reduce((a, b) => a + b, 0) +} + +console.log(sum.length) // 0 - rest parameters have length 0! + +// Our curry won't work correctly +const curriedSum = curry(sum) +curriedSum(1)(2)(3) // Calls immediately with just 1! +``` + +**Solution:** Specify arity explicitly: + +```javascript +const curryN = (fn, arity) => { + return function curried(...args) { + return args.length >= arity + ? fn(...args) + : (...next) => curried(...args, ...next) + } +} + +const curriedSum = curryN(sum, 3) +curriedSum(1)(2)(3) // 6 +``` + +--- + +## Common Composition Mistakes + +### Mistake #1: Type Mismatches in Pipeline + +Each function's output must match the next function's expected input: + +```javascript +const getName = obj => obj.name // Returns string +const getLength = arr => arr.length // Expects array! + +// ❌ Broken pipeline +const broken = pipe(getName, getLength) +broken({ name: 'Alice' }) // 5 (works by accident - string has .length) + +// But what if getName returns something without .length? +const getAge = obj => obj.age // Returns number +const getLength = arr => arr.length + +const reallyBroken = pipe(getAge, getLength) +reallyBroken({ age: 25 }) // undefined - numbers don't have .length +``` + +### Mistake #2: Side Effects in Pipelines + +Composed functions should be [pure](/concepts/pure-functions). Side effects make pipelines unpredictable: + +```javascript +// ❌ Side effect in pipeline +let globalCounter = 0 +const addAndCount = x => { + globalCounter++ // Side effect! + return x + 1 +} + +// This is unpredictable - depends on global state +const process = pipe(addAndCount, addAndCount) +``` + +### Mistake #3: Over-Composing + +Sometimes explicit code is clearer than a point-free pipeline: + +```javascript +// ❌ Too clever - hard to understand +const processUser = pipe( + prop('account'), + prop('settings'), + prop('preferences'), + prop('theme'), + defaultTo('light'), + eq('dark'), + ifElse(identity, always('🌙'), always('☀️')) +) + +// ✓ Clearer +function getThemeEmoji(user) { + const theme = user?.account?.settings?.preferences?.theme ?? 'light' + return theme === 'dark' ? '🌙' : '☀️' +} +``` + + +**Rule of thumb:** Use composition when it makes code clearer, not just shorter. If a colleague would struggle to understand your pipeline, consider a more explicit approach. + + +--- + +## Key Takeaways + + +**The key things to remember:** + +1. **Currying transforms `f(a, b, c)` into `f(a)(b)(c)`** — each call takes one argument and returns a function waiting for the next + +2. **Currying depends on closures** — each nested function "closes over" arguments from parent functions, remembering them + +3. **Currying ≠ Partial Application** — currying always produces unary functions; partial application fixes some args and takes the rest together + +4. **Function composition combines simple functions into complex ones** — output of one becomes input of the next + +5. **`pipe()` flows left-to-right, `compose()` flows right-to-left** — most developers prefer pipe because it reads in execution order + +6. **Currying enables composition** — curried functions take one input and return one output, perfect for chaining + +7. **"Data-last" ordering is essential** — put the data parameter last so curried functions compose naturally + +8. **Point-free style focuses on transformations** — no explicit data parameters, just a chain of operations + +9. **Libraries like Lodash/Ramda add powerful features** — placeholders, auto-currying, and battle-tested utilities + +10. **Vanilla JS implementations work for most cases** — `curry`, `pipe`, and `compose` are just a few lines each + + +--- + +## Test Your Knowledge + + + + **Answer:** + + **Currying** transforms a function so that it takes arguments one at a time, returning a new function after each argument until all are received. + + **Partial application** fixes some arguments upfront and returns a function that takes the remaining arguments together. + + ```javascript + // Currying: one argument at a time + const curriedAdd = a => b => c => a + b + c + curriedAdd(1)(2)(3) // 6 + + // Partial application: fix some args, take rest together + const add = (a, b, c) => a + b + c + const partial = (fn, ...preset) => (...rest) => fn(...preset, ...rest) + const add1 = partial(add, 1) + add1(2, 3) // 6 - takes remaining args at once + ``` + + + + **Answer:** + + ```javascript + function curry(fn) { + return function curried(...args) { + if (args.length >= fn.length) { + return fn(...args) + } + return (...nextArgs) => curried(...args, ...nextArgs) + } + } + + // Usage + const add = (a, b, c) => a + b + c + const curriedAdd = curry(add) + + curriedAdd(1)(2)(3) // 6 + curriedAdd(1, 2)(3) // 6 + curriedAdd(1)(2, 3) // 6 + curriedAdd(1, 2, 3) // 6 + ``` + + + + **Answer:** + + Both combine functions, but in opposite directions: + + - **`pipe(f, g, h)(x)`** — Left to right: `h(g(f(x)))` + - **`compose(f, g, h)(x)`** — Right to left: `f(g(h(x)))` + + ```javascript + const add1 = x => x + 1 + const double = x => x * 2 + const square = x => x * x + + // pipe: add1 first, then double, then square + pipe(add1, double, square)(3) // ((3+1)*2)² = 64 + + // compose: square first, then double, then add1 + compose(add1, double, square)(3) // (3²*2)+1 = 19 + ``` + + Most developers prefer `pipe` because functions are listed in execution order. + + + + **Answer:** + + Composition works best with functions that take one input and return one output. Currying transforms multi-argument functions into chains of single-argument functions, making them perfect for composition. + + ```javascript + // Without currying - can't compose + const add = (a, b) => a + b + const multiply = (a, b) => a * b + // How would you pipe these? + + // With currying - composes naturally + const add = a => b => a + b + const multiply = a => b => a * b + + const add5 = add(5) + const double = multiply(2) + + const add5ThenDouble = pipe(add5, double) + add5ThenDouble(10) // 30 + ``` + + The key is "data-last" ordering: configure the function first, pass data last. + + + + **Answer:** + + This is a classic interview question. The trick is to return a function that can be called with more arguments OR returns the sum when called with no arguments: + + ```javascript + function sum(a) { + return function next(b) { + if (b === undefined) { + return a // No more arguments, return sum + } + return sum(a + b) // More arguments, keep accumulating + } + } + + sum(1)(2)(3)() // 6 + sum(1)(2)(3)(4)(5)() // 15 + sum(10)() // 10 + ``` + + Alternative using `valueOf` for implicit conversion: + + ```javascript + function sum(a) { + const fn = b => sum(a + b) + fn.valueOf = () => a + return fn + } + + +sum(1)(2)(3) // 6 (unary + triggers valueOf) + ``` + + + + **Answer:** + + Point-free style (also called "tacit programming") is writing functions without explicitly mentioning their arguments. Instead of defining what to do with data, you compose operations. + + ```javascript + // Pointed style (explicit argument) + const getUpperName = user => user.name.toUpperCase() + + // Point-free style (no explicit argument) + const getUpperName = pipe( + prop('name'), + toUpperCase + ) + + // Another example + // Pointed: + const doubleAll = numbers => numbers.map(x => x * 2) + + // Point-free: + const doubleAll = map(x => x * 2) + ``` + + Point-free code focuses on the transformations rather than the data being transformed. It's often more declarative and can be easier to reason about, but can also be harder to read if overused. + + + +--- + +## Related Concepts + + + + Currying depends on closures to remember arguments between calls + + + Functions that return functions are the foundation of currying + + + Composed pipelines work best with pure, side-effect-free functions + + + Array methods that compose beautifully when curried + + + +--- + +## Reference + + + + Complete guide to JavaScript functions, the building blocks of currying and composition + + + Understanding closures is essential for understanding how currying preserves arguments + + + The reduce method powers our compose and pipe implementations + + + Used in compose to process functions from right to left + + + +## Articles + + + + The definitive tutorial on currying with clear examples and an advanced curry implementation. Includes a practical logging example that shows real-world benefits. + + + Step-by-step debugger walkthrough showing exactly how pipe and compose work internally. The visual traces make the concept click. + + + Part of the "Composing Software" series. Comprehensive coverage of how currying enables composition, with trace utilities for debugging pipelines. + + + Free online chapter covering function inputs, currying, and partial application in depth. Kyle Simpson explains the nuances between currying and partial application better than almost anyone. + + + Index to the complete "Composing Software" series covering functional programming, composition, functors, and more in JavaScript. + + + Covers practical functional JavaScript patterns including currying and composition with approachable explanations. + + + +## Videos + + + + Mattias Petter Johansson's entertaining explanation of currying as part of his beloved functional programming series. Great for visual learners. + + + Kyle Cook's clear, beginner-friendly walkthrough of compose and pipe with practical examples you can follow along with. + + + A beloved JSUnconf talk that explains functional programming concepts with clarity and humor. Anjana's approachable style makes abstract concepts feel tangible. + + diff --git a/docs/concepts/data-structures.mdx b/docs/concepts/data-structures.mdx new file mode 100644 index 00000000..33d2147d --- /dev/null +++ b/docs/concepts/data-structures.mdx @@ -0,0 +1,1258 @@ +--- +title: "Data Structures: Organizing and Storing Data in JavaScript" +sidebarTitle: "Data Structures: Organizing and Storing Data" +description: "Learn JavaScript data structures from built-in Arrays, Objects, Maps, and Sets to implementing Stacks, Queues, and Linked Lists. Understand when to use each structure." +--- + +Why does finding an item in an array take longer as it grows? Why can you look up an object property instantly regardless of how many properties it has? The answer lies in **data structures**. + +```javascript +// Array: searching gets slower as the array grows +const users = ['alice', 'bob', 'charlie', /* ...thousands more */] +users.includes('zara') // Has to check every element - O(n) + +// Object: lookup is instant regardless of size +const userMap = { alice: 1, bob: 2, charlie: 3, /* ...thousands more */ } +userMap['zara'] // Direct access - O(1) +``` + +A **data structure** is a way of organizing data so it can be used efficiently. The right structure makes your code faster and cleaner. The wrong one can make simple operations painfully slow. + + +**What you'll learn in this guide:** +- JavaScript's built-in structures: Array, Object, Map, Set, WeakMap, WeakSet +- When to use each built-in structure +- How to implement: Stack, Queue, Linked List, Binary Search Tree +- Choosing the right data structure for the job +- Common interview questions and patterns + + + +**Prerequisites:** This guide shows time complexity (like O(1) and O(n)) for operations. If you're not familiar with Big O notation, check out our [Algorithms & Big O guide](/concepts/algorithms-big-o) first. We also use [classes](/concepts/factories-classes) for implementations. + + +--- + +## What Are Data Structures? + +Think of data structures like different ways to organize a library. You could: + +- **Stack books on a table** — Easy to add/remove from the top, but finding a specific book means digging through the pile +- **Line them up on a shelf** — Easy to browse in order, but adding a book in the middle means shifting everything +- **Organize by category with an index** — Finding any book is fast, but you need to maintain the index + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ DATA STRUCTURE TRADE-OFFS │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ARRAY OBJECT/MAP LINKED LIST │ +│ ┌─┬─┬─┬─┬─┐ ┌────────────┐ ┌───┐ ┌───┐ │ +│ │0│1│2│3│4│ │ key: value │ │ A │──►│ B │──► │ +│ └─┴─┴─┴─┴─┘ │ key: value │ └───┘ └───┘ │ +│ └────────────┘ │ +│ ✓ Fast index access ✓ Fast key lookup ✓ Fast insert/delete │ +│ ✓ Ordered ✓ Flexible keys ✗ Slow search │ +│ ✗ Slow insert in middle ✗ No order (Object) ✗ No index access │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +Every data structure has trade-offs. Your job is to pick the one that makes your most frequent operations fast. + +--- + +## JavaScript's Built-in Data Structures + +JavaScript gives you several data structures out of the box. Let's look at each one. + +### Arrays + +An **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)** is an ordered collection of values, accessed by numeric index. It's the most common data structure in JavaScript. + +```javascript +const fruits = ['apple', 'banana', 'cherry'] + +// Access by index - O(1) +fruits[0] // 'apple' + +// Add to end - O(1) +fruits.push('date') // ['apple', 'banana', 'cherry', 'date'] + +// Remove from end - O(1) +fruits.pop() // 'date' + +// Add to beginning - O(n) - shifts all elements! +fruits.unshift('apricot') // ['apricot', 'apple', 'banana', 'cherry'] + +// Search - O(n) +fruits.indexOf('banana') // 3 +fruits.includes('mango') // false +``` + +**Time Complexity:** + +| Operation | Method | Complexity | Why | +|-----------|--------|------------|-----| +| Access by index | `arr[i]` | O(1) | Direct memory access | +| Add/remove at end | `push()`, `pop()` | O(1) | No shifting needed | +| Add/remove at start | `unshift()`, `shift()` | O(n) | Must shift all elements | +| Search | `indexOf()`, `includes()` | O(n) | Must check each element | +| Insert in middle | `splice()` | O(n) | Must shift elements after | + +**When to use Arrays:** +- You need ordered data +- You access elements by position +- You mostly add/remove from the end +- You need to iterate over all elements + +--- + +### Objects + +An **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** stores key-value pairs where keys are strings or Symbols. It's JavaScript's fundamental way to group related data. + +```javascript +const user = { + name: 'Alice', + age: 30, + email: 'alice@example.com' +} + +// Access - O(1) +user.name // 'Alice' +user['age'] // 30 + +// Add/Update - O(1) +user.role = 'admin' + +// Delete - O(1) +delete user.email + +// Check if key exists - O(1) +'name' in user // true +user.hasOwnProperty('name') // true +``` + +**Limitations of Objects:** +- Keys are converted to strings (numbers become "1", "2", etc.) +- Objects have a prototype chain (inherited properties) +- No built-in `.size` property +- Property order is preserved in ES2015+, but with specific rules: integer keys are sorted numerically first, then string keys appear in insertion order + +**When to use Objects:** +- Storing entity data (user profiles, settings) +- When keys are known strings +- Configuration objects +- JSON data + +--- + +### Map + +A **[Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map)** is like an Object but with superpowers: keys can be *any* type, it maintains insertion order, and has a `.size` property. + +```javascript +const map = new Map() + +// Keys can be ANY type +map.set('string', 'works') +map.set(123, 'number key') +map.set({ id: 1 }, 'object key') +map.set(true, 'boolean key') + +// Access - O(1) +map.get('string') // 'works' +map.get(123) // 'number key' + +// Size is built-in +map.size // 4 + +// Check existence - O(1) +map.has('string') // true + +// Delete - O(1) +map.delete(123) + +// Iteration (maintains insertion order) +for (const [key, value] of map) { + console.log(key, value) +} +``` + +**Map vs Object:** + +| Feature | Map | Object | +|---------|-----|--------| +| Key types | Any | String or Symbol | +| Order | Guaranteed insertion order | Preserved (integer keys sorted first) | +| Size | `map.size` | `Object.keys(obj).length` | +| Iteration | Directly iterable | Need `Object.keys()` | +| Performance | Better for frequent add/delete | Better for static data | +| Prototype | None | Has prototype chain | + +**When to use Map:** +- Keys aren't strings (objects, functions, etc.) +- You need to know the size frequently +- You add/delete keys often +- Order matters + +```javascript +// Common use: counting occurrences +function countWords(text) { + const words = text.toLowerCase().split(/\s+/) + const counts = new Map() + + for (const word of words) { + counts.set(word, (counts.get(word) || 0) + 1) + } + + return counts +} + +countWords('the cat and the dog') +// Map { 'the' => 2, 'cat' => 1, 'and' => 1, 'dog' => 1 } +``` + +--- + +### Set + +A **[Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set)** stores unique values. Duplicates are automatically ignored. + +```javascript +const set = new Set() + +// Add values - O(1) +set.add(1) +set.add(2) +set.add(2) // Ignored - already exists +set.add('hello') + +set.size // 3 (not 4!) + +// Check existence - O(1) +set.has(2) // true + +// Delete - O(1) +set.delete(1) + +// Iteration +for (const value of set) { + console.log(value) +} +``` + +**The classic use case: removing duplicates** + +```javascript +const numbers = [1, 2, 2, 3, 3, 3, 4] +const unique = [...new Set(numbers)] // [1, 2, 3, 4] +``` + +**Set Operations (ES2024+):** + + +These methods are part of ES2024 and are supported in all modern browsers as of late 2024. Check [browser compatibility](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set#browser_compatibility) if you need to support older browsers. + + +```javascript +const a = new Set([1, 2, 3]) +const b = new Set([2, 3, 4]) + +// Union: elements in either set +a.union(b) // Set {1, 2, 3, 4} + +// Intersection: elements in both sets +a.intersection(b) // Set {2, 3} + +// Difference: elements in a but not in b +a.difference(b) // Set {1} + +// Symmetric difference: elements in either but not both +a.symmetricDifference(b) // Set {1, 4} + +// Subset check +new Set([1, 2]).isSubsetOf(a) // true +``` + +**When to use Set:** +- You need unique values +- You check "does this exist?" frequently +- Removing duplicates from arrays +- Tracking visited items + +--- + +### WeakMap and WeakSet + + + +**[WeakMap](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap)** and **[WeakSet](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet)** are special versions where keys (WeakMap) or values (WeakSet) are held "weakly." This means they don't prevent garbage collection. + +**WeakMap:** +- Keys must be objects (or non-registered symbols) +- If the key object has no other references, it gets garbage collected +- Not iterable (no `.keys()`, `.values()`, `.forEach()`) +- No `.size` property + +```javascript +const privateData = new WeakMap() + +class User { + constructor(name, password) { + this.name = name + // Store private data that can't be accessed externally + privateData.set(this, { password }) + } + + checkPassword(input) { + return privateData.get(this).password === input + } +} + +const user = new User('Alice', 'secret123') +user.name // 'Alice' +user.password // undefined - it's private! +user.checkPassword('secret123') // true + +// When 'user' is garbage collected, the private data is too +``` + +**WeakSet:** +- Values must be objects +- Useful for tracking which objects have been processed + +```javascript +const processed = new WeakSet() + +function processOnce(obj) { + if (processed.has(obj)) { + return // Already processed + } + + processed.add(obj) + // Do expensive processing... +} +``` + +**When to use Weak versions:** +- Caching computed data for objects +- Storing private instance data +- Tracking objects without preventing garbage collection + + + +--- + +## Implementing Common Data Structures + +JavaScript doesn't have built-in Stack, Queue, or Linked List classes, but they're easy to implement and important to understand. + +### Stack (LIFO) + +A **Stack** follows Last-In-First-Out: the last item added is the first removed. Think of a stack of plates. + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ STACK (LIFO) │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ push(4) pop() │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌───┐ ┌───┐ │ +│ │ 4 │ ◄─ top │ │ │ +│ ├───┤ ├───┤ │ +│ │ 3 │ │ 3 │ ◄─ top │ +│ ├───┤ ├───┤ │ +│ │ 2 │ │ 2 │ │ +│ ├───┤ ├───┤ │ +│ │ 1 │ │ 1 │ │ +│ └───┘ └───┘ │ +│ │ +│ "Last in, first out" - like a stack of plates │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +**Real-world uses:** +- Browser history (back button) +- Undo/redo functionality +- Function call stack +- Expression evaluation (parentheses matching) + +**Implementation:** + +```javascript +class Stack { + constructor() { + this.items = [] + } + + push(item) { + this.items.push(item) + } + + pop() { + return this.items.pop() + } + + peek() { + return this.items[this.items.length - 1] + } + + isEmpty() { + return this.items.length === 0 + } + + size() { + return this.items.length + } +} + +// Usage +const stack = new Stack() +stack.push(1) +stack.push(2) +stack.push(3) +stack.peek() // 3 (look at top without removing) +stack.pop() // 3 +stack.pop() // 2 +stack.size() // 1 +``` + +**Time Complexity:** All operations are O(1). + +--- + +### Queue (FIFO) + +A **Queue** follows First-In-First-Out: the first item added is the first removed. Think of a line at a store. + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ QUEUE (FIFO) │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ enqueue(4) dequeue() │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌───┬───┬───┬───┐ ┌───┬───┬───┐ │ +│ │ 4 │ 3 │ 2 │ 1 │ ───────────────────────► │ 4 │ 3 │ 2 │ │ +│ └───┴───┴───┴───┘ └───┴───┴───┘ │ +│ back front back front │ +│ │ +│ "First in, first out" - like a line at a store │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +**Real-world uses:** +- Task scheduling +- Print queue +- BFS graph traversal +- Message queues + +**Implementation:** + +```javascript +class Queue { + constructor() { + this.items = [] + } + + enqueue(item) { + this.items.push(item) + } + + dequeue() { + return this.items.shift() // Note: O(n) with arrays! + } + + front() { + return this.items[0] + } + + isEmpty() { + return this.items.length === 0 + } + + size() { + return this.items.length + } +} + +// Usage +const queue = new Queue() +queue.enqueue('first') +queue.enqueue('second') +queue.enqueue('third') +queue.dequeue() // 'first' +queue.front() // 'second' +``` + + +**Performance note:** Using `shift()` on an array is O(n) because all remaining elements must be re-indexed. For performance-critical code, use a linked list implementation or an object with head/tail pointers. + + +--- + +### Linked List + +A **Linked List** is a chain of nodes where each node points to the next. Unlike arrays, elements aren't stored in contiguous memory. + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ LINKED LIST │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ head tail │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ value: 1 │ │ value: 2 │ │ value: 3 │ │ value: 4 │ │ +│ │ next: ───────► │ next: ───────► │ next: ───────► │ next: null│ │ +│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ +│ │ +│ Nodes can be anywhere in memory - connected by references │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +**Linked List vs Array:** + +| Operation | Array | Linked List | +|-----------|-------|-------------| +| Access by index | O(1) | O(n) | +| Insert at beginning | O(n) | O(1) | +| Insert at end | O(1) | O(1) with tail pointer | +| Insert in middle | O(n) | O(1) if you have the node | +| Search | O(n) | O(n) | + +**Implementation:** + +```javascript +class Node { + constructor(value) { + this.value = value + this.next = null + } +} + +class LinkedList { + constructor() { + this.head = null + this.size = 0 + } + + // Add to beginning - O(1) + prepend(value) { + const node = new Node(value) + node.next = this.head + this.head = node + this.size++ + } + + // Add to end - O(n) + append(value) { + const node = new Node(value) + + if (!this.head) { + this.head = node + } else { + let current = this.head + while (current.next) { + current = current.next + } + current.next = node + } + this.size++ + } + + // Find a value - O(n) + find(value) { + let current = this.head + while (current) { + if (current.value === value) { + return current + } + current = current.next + } + return null + } + + // Convert to array for easy viewing + toArray() { + const result = [] + let current = this.head + while (current) { + result.push(current.value) + current = current.next + } + return result + } +} + +// Usage +const list = new LinkedList() +list.prepend(1) +list.append(2) +list.append(3) +list.prepend(0) +list.toArray() // [0, 1, 2, 3] +list.find(2) // Node { value: 2, next: Node } +``` + +**When to use Linked Lists:** +- Frequent insertions/deletions at the beginning +- You don't need random access by index +- Implementing queues (for O(1) dequeue) + +--- + +### Binary Search Tree + +A **Binary Search Tree (BST)** is a hierarchical structure where each node has at most two children. The left child is smaller, the right child is larger. + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ BINARY SEARCH TREE │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌────┐ │ +│ │ 10 │ ◄─ root │ +│ └────┘ │ +│ / \ │ +│ ┌────┐ ┌────┐ │ +│ │ 5 │ │ 15 │ │ +│ └────┘ └────┘ │ +│ / \ \ │ +│ ┌────┐ ┌────┐ ┌────┐ │ +│ │ 3 │ │ 7 │ │ 20 │ │ +│ └────┘ └────┘ └────┘ │ +│ │ +│ Rule: left child < parent < right child │ +│ This makes searching fast: just go left or right! │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +**Time Complexity:** + +| Operation | Average | Worst (unbalanced) | +|-----------|---------|-------------------| +| Search | O(log n) | O(n) | +| Insert | O(log n) | O(n) | +| Delete | O(log n) | O(n) | + +**Implementation:** + +```javascript +class TreeNode { + constructor(value) { + this.value = value + this.left = null + this.right = null + } +} + +class BinarySearchTree { + constructor() { + this.root = null + } + + insert(value) { + const node = new TreeNode(value) + + if (!this.root) { + this.root = node + return + } + + let current = this.root + while (true) { + if (value < current.value) { + // Go left + if (!current.left) { + current.left = node + return + } + current = current.left + } else { + // Go right + if (!current.right) { + current.right = node + return + } + current = current.right + } + } + } + + search(value) { + let current = this.root + + while (current) { + if (value === current.value) { + return current + } + current = value < current.value ? current.left : current.right + } + + return null + } + + // In-order traversal: left, root, right (gives sorted order) + inOrder(node = this.root, result = []) { + if (node) { + this.inOrder(node.left, result) + result.push(node.value) + this.inOrder(node.right, result) + } + return result + } +} + +// Usage +const bst = new BinarySearchTree() +bst.insert(10) +bst.insert(5) +bst.insert(15) +bst.insert(3) +bst.insert(7) +bst.insert(20) + +bst.search(7) // TreeNode { value: 7, ... } +bst.search(100) // null +bst.inOrder() // [3, 5, 7, 10, 15, 20] - sorted! +``` + +**When to use BST:** +- You need fast search, insert, and delete (O(log n) average) +- Data needs to stay sorted +- Implementing autocomplete, spell checkers + +--- + +### Graph + + + +A **Graph** consists of nodes (vertices) connected by edges. Think social networks (people connected by friendships) or maps (cities connected by roads). + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ GRAPH │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ A ─────── B │ +│ /│\ │ │ +│ / │ \ │ │ +│ / │ \ │ │ +│ C │ D ────┘ │ +│ \ │ / │ +│ \ │ / │ +│ \│/ │ +│ E │ +│ │ +│ Adjacency List representation: │ +│ A: [B, C, D, E] │ +│ B: [A, D] │ +│ C: [A, E] │ +│ ... │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +**Basic Implementation (Adjacency List):** + +```javascript +class Graph { + constructor() { + this.adjacencyList = new Map() + } + + addVertex(vertex) { + if (!this.adjacencyList.has(vertex)) { + this.adjacencyList.set(vertex, []) + } + } + + addEdge(v1, v2) { + this.adjacencyList.get(v1).push(v2) + this.adjacencyList.get(v2).push(v1) // For undirected graph + } + + // Breadth-First Search - uses Queue (FIFO) + bfs(start) { + const visited = new Set() + const queue = [start] + const result = [] + + while (queue.length) { + const vertex = queue.shift() + if (visited.has(vertex)) continue + + visited.add(vertex) + result.push(vertex) + + for (const neighbor of this.adjacencyList.get(vertex)) { + if (!visited.has(neighbor)) { + queue.push(neighbor) + } + } + } + + return result + } + + // Depth-First Search - uses Stack (LIFO) via recursion + dfs(start, visited = new Set(), result = []) { + if (visited.has(start)) return result + + visited.add(start) + result.push(start) + + for (const neighbor of this.adjacencyList.get(start)) { + this.dfs(neighbor, visited, result) + } + + return result + } +} + +// Usage +const graph = new Graph() +graph.addVertex('A') +graph.addVertex('B') +graph.addVertex('C') +graph.addEdge('A', 'B') +graph.addEdge('A', 'C') +graph.addEdge('B', 'C') +graph.bfs('A') // ['A', 'B', 'C'] - level by level +graph.dfs('A') // ['A', 'B', 'C'] - goes deep first +``` + +**Real-world uses:** +- Social networks (friend connections) +- Maps and navigation (shortest path) +- Recommendation systems +- Dependency resolution (package managers) + + + +--- + +## Choosing the Right Data Structure + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ WHICH DATA STRUCTURE SHOULD I USE? │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Need ordered data with index access? │ +│ └──► ARRAY │ +│ │ +│ Need key-value pairs with string keys? │ +│ └──► OBJECT (static data) or MAP (dynamic) │ +│ │ +│ Need key-value with any type as key? │ +│ └──► MAP │ +│ │ +│ Need unique values only? │ +│ └──► SET │ +│ │ +│ Need LIFO (last in, first out)? │ +│ └──► STACK │ +│ │ +│ Need FIFO (first in, first out)? │ +│ └──► QUEUE │ +│ │ +│ Need fast insert/delete at beginning? │ +│ └──► LINKED LIST │ +│ │ +│ Need fast search + sorted data? │ +│ └──► BINARY SEARCH TREE │ +│ │ +│ Modeling relationships/connections? │ +│ └──► GRAPH │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +| Use Case | Best Structure | Why | +|----------|----------------|-----| +| Todo list | Array | Ordered, index access | +| User settings | Object | String keys, static | +| Word frequency counter | Map | Easy increment, any key | +| Tag system | Set | Unique values | +| Browser back button | Stack | LIFO | +| Task scheduler | Queue | FIFO | +| Playlist with prev/next | Linked List (doubly) | O(1) traversal | +| Dictionary/autocomplete | Trie | Fast prefix search | +| Social network | Graph | Connections | + +--- + +## Common Interview Questions + +Interview questions often test your understanding of data structures. Here are patterns you'll encounter: + + + + **Problem:** Find two numbers in an array that add up to a target. + + **Approach:** Use a Map to store numbers you've seen. For each number, check if `target - number` exists in the Map. + + ```javascript + function twoSum(nums, target) { + const seen = new Map() + + for (let i = 0; i < nums.length; i++) { + const complement = target - nums[i] + + if (seen.has(complement)) { + return [seen.get(complement), i] + } + + seen.set(nums[i], i) + } + + return [] + } + + twoSum([2, 7, 11, 15], 9) // [0, 1] + ``` + + **Why Map?** O(1) lookup turns O(n²) brute force into O(n). + + + + **Problem:** Check if a string of brackets is valid: `()[]{}`. + + **Approach:** Push opening brackets onto stack. When you see a closing bracket, pop and check if it matches. + + ```javascript + function isValid(s) { + const stack = [] + const pairs = { ')': '(', ']': '[', '}': '{' } + + for (const char of s) { + if (char in pairs) { + // Closing bracket - check if it matches + if (stack.pop() !== pairs[char]) { + return false + } + } else { + // Opening bracket - push to stack + stack.push(char) + } + } + + return stack.length === 0 + } + + isValid('([{}])') // true + isValid('([)]') // false + ``` + + + + **Problem:** Reverse a linked list. + + **Approach:** Keep track of previous, current, and next. Reverse pointers as you go. + + ```javascript + function reverseList(head) { + let prev = null + let current = head + + while (current) { + const next = current.next // Save next + current.next = prev // Reverse pointer + prev = current // Move prev forward + current = next // Move current forward + } + + return prev // New head + } + ``` + + **Key insight:** You need three pointers to avoid losing references. + + + + **Problem:** Determine if a linked list has a cycle. + + **Approach:** Floyd's Tortoise and Hare - use two pointers, one fast (2 steps) and one slow (1 step). If they meet, there's a cycle. + + ```javascript + function hasCycle(head) { + let slow = head + let fast = head + + while (fast && fast.next) { + slow = slow.next + fast = fast.next.next + + if (slow === fast) { + return true // They met - cycle exists + } + } + + return false // Fast reached end - no cycle + } + ``` + + **Why this works:** In a cycle, the fast pointer will eventually "lap" the slow pointer. + + + + **Problem:** Find the maximum depth of a binary tree. + + **Approach:** Recursively find the depth of left and right subtrees, take the max. + + ```javascript + function maxDepth(root) { + if (!root) return 0 + + const leftDepth = maxDepth(root.left) + const rightDepth = maxDepth(root.right) + + return Math.max(leftDepth, rightDepth) + 1 + } + ``` + + **Base case:** Empty tree has depth 0. + + + + **Problem:** Implement a queue using only stacks. + + **Approach:** Use two stacks. Push to stack1. For dequeue, if stack2 is empty, pour all of stack1 into stack2 (reversing order), then pop from stack2. + + ```javascript + class QueueFromStacks { + constructor() { + this.stack1 = [] // For enqueue + this.stack2 = [] // For dequeue + } + + enqueue(item) { + this.stack1.push(item) + } + + dequeue() { + if (this.stack2.length === 0) { + // Pour stack1 into stack2 + while (this.stack1.length) { + this.stack2.push(this.stack1.pop()) + } + } + return this.stack2.pop() + } + } + ``` + + **Amortized O(1):** Each element is moved at most twice. + + + +--- + +## Key Takeaways + + +**The key things to remember:** + +1. **Arrays** are great for ordered data with index access. Push/pop are O(1), but shift/unshift are O(n). + +2. **Objects** store string-keyed data. Use them for static configuration and entity data. + +3. **Map** is the better choice when keys aren't strings, you need `.size`, or you add/delete frequently. + +4. **Set** stores unique values. The `[...new Set(arr)]` trick removes duplicates instantly. + +5. **Stack (LIFO)** is perfect for undo/redo, parsing expressions, and DFS traversal. + +6. **Queue (FIFO)** is ideal for task scheduling and BFS traversal. Use a linked list for O(1) dequeue. + +7. **Linked Lists** excel at insertions/deletions but lack random access. Use when you frequently modify the beginning. + +8. **Binary Search Trees** give O(log n) search/insert/delete on average. They keep data sorted. + +9. **Choose based on your most frequent operation.** What makes one structure fast makes another slow. + +10. **Interview tip:** When you need O(1) lookup, think Map or Set. When you need to track order of operations, think Stack or Queue. + + +--- + +## Test Your Knowledge + + + + **Answer:** + + Use Map when: + - Keys are not strings (objects, numbers, etc.) + - You need to know the size frequently (`.size` vs `Object.keys().length`) + - You add/delete keys often (Map is optimized for this) + - You need guaranteed insertion order + - You want to avoid prototype chain issues + + Use Object when: + - Keys are known strings + - You're working with JSON data + - You need object destructuring or spread syntax + + + + **Answer:** + + `pop()` removes from the end. No other elements need to move. + + `shift()` removes from the beginning. Every remaining element must be re-indexed: + - Element at index 1 moves to 0 + - Element at index 2 moves to 1 + - ...and so on + + This is why Queue implementations with arrays have O(n) dequeue. For O(1), use a linked list or object with head/tail pointers. + + + + **Answer:** + + **Stack (LIFO):** Last In, First Out + - Like a stack of plates - you take from the top + - `push()` and `pop()` operate on the same end + - Use for: undo/redo, back button, recursion + + **Queue (FIFO):** First In, First Out + - Like a line at a store - first person in line is served first + - `enqueue()` adds to back, `dequeue()` removes from front + - Use for: task scheduling, BFS, print queues + + + + **Answer:** + + Linked List wins when: + - You frequently insert/delete at the beginning (O(1) vs O(n)) + - You don't need random access by index + - You're implementing a queue (O(1) dequeue) + - Memory is fragmented (nodes can be anywhere) + + Array wins when: + - You need index-based access + - You iterate sequentially often + - You mostly add/remove from the end + - You need `.length`, `.map()`, `.filter()`, etc. + + + + **Answer:** + + BSTs use the rule: left < parent < right. This means: + + - To find a value, compare with root + - If smaller, go left; if larger, go right + - Each comparison eliminates half the remaining nodes + + This gives O(log n) search, insert, and delete (on average). + + **Catch:** If you insert sorted data, the tree becomes a linked list (all nodes on one side), and operations become O(n). Self-balancing trees (AVL, Red-Black) solve this. + + + + **Answer:** + + The cleanest way is with Set: + + ```javascript + const unique = [...new Set(array)] + ``` + + This works because: + 1. `new Set(array)` creates a Set (which only keeps unique values) + 2. `[...set]` spreads the Set back into an array + + Time complexity: O(n) - each element is processed once. + + + +--- + +## Related Concepts + + + + Understanding time complexity helps you choose the right data structure + + + The class syntax used to implement data structures + + + Array methods like map, filter, and reduce + + + Essential for tree and graph traversal algorithms + + + +--- + +## Reference + + + + Complete reference for JavaScript arrays + + + Documentation for Object methods and properties + + + Guide to the Map collection type + + + Documentation for Set and its new ES2024 methods + + + When to use WeakMap for memory management + + + MDN's overview of JavaScript data types and structures + + + +## Articles + + + + The clearest explanation of Map and Set with interactive examples. Covers WeakMap and WeakSet too. + + + Oleksii Trekhleb's legendary GitHub repo with implementations of every data structure and algorithm in JavaScript. Over 180k stars for good reason. + + + freeCodeCamp's practical guide covering arrays through graphs with real-world examples you can follow along with. + + + Jamie Kyle's annotated source code explaining data structures in ~200 lines. Perfect if you learn by reading well-commented code. + + + +## Videos + + + + freeCodeCamp's complete 8-hour course covering everything from Big O to graph algorithms. Great for interview prep. + + + Academind's beginner-friendly introduction focusing on when and why to use each structure, not just how. + + + William Fiset's comprehensive course with animations that make complex structures like trees and graphs click. + + diff --git a/docs/concepts/design-patterns.mdx b/docs/concepts/design-patterns.mdx new file mode 100644 index 00000000..c6cf3578 --- /dev/null +++ b/docs/concepts/design-patterns.mdx @@ -0,0 +1,1122 @@ +--- +title: "Design Patterns: Reusable Solutions to Common Problems in JavaScript" +sidebarTitle: "Design Patterns: Reusable Solutions" +description: "Learn JavaScript design patterns like Module, Singleton, Observer, Factory, Proxy, and Decorator. Understand when to use each pattern and avoid common pitfalls." +--- + +Ever find yourself solving the same problem over and over? What if experienced developers already figured out the best solutions to these recurring challenges? + +```javascript +// The Observer pattern — notify multiple listeners when something happens +const newsletter = { + subscribers: [], + + subscribe(callback) { + this.subscribers.push(callback) + }, + + publish(article) { + this.subscribers.forEach(callback => callback(article)) + } +} + +// Anyone can subscribe +newsletter.subscribe(article => console.log(`New article: ${article}`)) +newsletter.subscribe(article => console.log(`Saving "${article}" for later`)) + +// When we publish, all subscribers get notified +newsletter.publish("Design Patterns in JavaScript") +// "New article: Design Patterns in JavaScript" +// "Saving "Design Patterns in JavaScript" for later" +``` + +**Design patterns** are proven solutions to common problems in software design. They're not code you copy-paste. They're templates, blueprints, or recipes that you adapt to solve specific problems in your own code. Learning patterns gives you a vocabulary to discuss solutions with other developers and helps you recognize when a well-known solution fits your problem. + + +**What you'll learn in this guide:** +- What design patterns are and why they matter +- The Module pattern for organizing code with private state +- The Singleton pattern (and why it's often unnecessary in JavaScript) +- The Factory pattern for creating objects dynamically +- The Observer pattern for event-driven programming +- The Proxy pattern for controlling object access +- The Decorator pattern for adding behavior without modification +- How to choose the right pattern for your problem + + + +**Prerequisites:** This guide assumes you understand [Factories and Classes](/concepts/factories-classes) and [IIFE, Modules & Namespaces](/concepts/iife-modules). Design patterns build on these object-oriented and modular programming concepts. + + +--- + +## The Toolkit Analogy + +Think of design patterns like specialized tools in a toolkit. A general-purpose hammer works for many tasks, but sometimes you need a specific tool: a Phillips screwdriver for certain screws, a wrench for bolts, or pliers for gripping. + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ DESIGN PATTERNS TOOLKIT │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ CREATIONAL STRUCTURAL BEHAVIORAL │ +│ ─────────── ────────── ────────── │ +│ How objects How objects How objects │ +│ are created are composed communicate │ +│ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ Singleton │ │ Proxy │ │ Observer │ │ +│ │ Factory │ │ Decorator │ │ │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ │ +│ Use when you need Use when you need Use when objects │ +│ to control object to wrap or extend need to react to │ +│ creation objects changes in others │ +│ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ MODULE (JS-specific) — Encapsulates code with private state │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +You don't use every tool for every job. Similarly, you don't use every pattern in every project. The skill is recognizing when a pattern fits your problem. + +--- + +## What Are Design Patterns? + +Design patterns are typical solutions to commonly occurring problems in software design. The term was popularized by the "Gang of Four" (GoF) in their 1994 book *Design Patterns: Elements of Reusable Object-Oriented Software*. They catalogued 23 patterns that developers kept reinventing. + +### Why JavaScript Is Different + +The original GoF patterns were written for languages like C++ and Smalltalk. JavaScript is different: + +| Feature | Impact on Patterns | +|---------|-------------------| +| **First-class functions** | Many patterns simplify to just passing functions around | +| **Prototypal inheritance** | No need for complex class hierarchies | +| **ES Modules** | Built-in module system replaces manual Module pattern | +| **Dynamic typing** | No need for interface abstractions | +| **Closures** | Natural way to create private state | + +This means some classical patterns are overkill in JavaScript, while others become more elegant. We'll focus on the patterns that are genuinely useful in modern JavaScript. + +### The Three Categories + +The original GoF patterns are grouped into three categories: + +1. **Creational Patterns** — Control how objects are created + - Singleton, Factory Method, Abstract Factory, Builder, Prototype + +2. **Structural Patterns** — Control how objects are composed + - Proxy, Decorator, Adapter, Facade, Bridge, Composite, Flyweight + +3. **Behavioral Patterns** — Control how objects communicate + - Observer, Strategy, Command, Mediator, Iterator, State, and others + + +**JavaScript-specific patterns:** The **Module pattern** isn't one of the original 23 GoF patterns. It's a JavaScript idiom that emerged to solve JavaScript-specific problems (like the lack of built-in modules before ES6). We include it here because it's essential for JavaScript developers. + + +We'll cover six patterns that are particularly useful in JavaScript: **Module** (JS-specific), **Singleton**, **Factory**, **Observer**, **Proxy**, and **Decorator**. + +--- + +## The Module Pattern + +The **Module pattern** encapsulates code into reusable units with private and public parts. Before ES6 modules existed, developers used IIFEs (Immediately Invoked Function Expressions) to create this pattern. Today, JavaScript has built-in [ES Modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) that provide this naturally. + +### ES6 Modules: The Modern Approach + +Each file is its own module. Variables are private unless you [`export`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export) them: + +```javascript +// counter.js — A module with private state +let count = 0 // Private — not exported, not accessible outside + +export function increment() { + count++ + return count +} + +export function decrement() { + count-- + return count +} + +export function getCount() { + return count +} + +// main.js — Using the module +import { increment, getCount } from './counter.js' + +increment() +increment() +console.log(getCount()) // 2 + +// Trying to access private state +// console.log(count) // ReferenceError: count is not defined +``` + +### The Classic IIFE Module Pattern + +Before ES6, developers used closures to create modules: + +```javascript +// The revealing module pattern using IIFE +const Counter = (function() { + // Private variables and functions + let count = 0 + + function logChange(action) { + console.log(`Counter ${action}: ${count}`) + } + + // Public API — "revealed" by returning an object + return { + increment() { + count++ + logChange('incremented') + return count + }, + decrement() { + count-- + logChange('decremented') + return count + }, + getCount() { + return count + } + } +})() + +Counter.increment() // "Counter incremented: 1" +Counter.increment() // "Counter incremented: 2" +console.log(Counter.getCount()) // 2 + +// Private members are truly private +console.log(Counter.count) // undefined +console.log(Counter.logChange) // undefined +``` + +### When to Use the Module Pattern + + + + Group related functions and data together. A `UserService` module might contain `login()`, `logout()`, `getCurrentUser()`, and private token storage. + + + + Expose only what consumers need. Internal helper functions, validation logic, and caching mechanisms stay private. + + + + Instead of 50 global functions, you have one module export. This prevents naming collisions with other code. + + + + +**Modern JavaScript:** Use ES6 modules (`import`/`export`) for new projects. The IIFE pattern is mainly for legacy code or environments without module support. See [IIFE, Modules & Namespaces](/concepts/iife-modules) for a deeper dive. + + +--- + +## The Singleton Pattern + +The **Singleton pattern** ensures a class has only one instance and provides a global access point to that instance. According to [Refactoring Guru](https://refactoring.guru/design-patterns/singleton), it solves two problems: guaranteeing a single instance and providing global access to it. + +### JavaScript Implementation + +```javascript +// Singleton using Object.freeze — immutable configuration +const Config = { + apiUrl: 'https://api.example.com', + timeout: 5000, + debug: false +} + +Object.freeze(Config) // Prevent all modifications + +// Usage anywhere in your app +console.log(Config.apiUrl) // "https://api.example.com" + +// Attempting to modify throws an error in strict mode (silently fails otherwise) +Config.apiUrl = 'https://evil.com' +console.log(Config.apiUrl) // Still "https://api.example.com" + +Config.debug = true +console.log(Config.debug) // Still false — frozen objects are immutable +``` + +### Class-Based Singleton + +```javascript +let instance = null + +class Database { + constructor() { + if (instance) { + return instance // Return existing instance + } + + this.connection = null + instance = this + } + + connect(url) { + if (!this.connection) { + this.connection = `Connected to ${url}` + console.log(this.connection) + } + return this.connection + } +} + +const db1 = new Database() +const db2 = new Database() + +console.log(db1 === db2) // true — Same instance! + +db1.connect('mongodb://localhost') // "Connected to mongodb://localhost" +db2.connect('mongodb://other') // Returns same connection, doesn't reconnect +``` + +### Why Singleton Is Often an Anti-Pattern in JavaScript + +Here's the thing: **Singletons are often unnecessary in JavaScript**. Here's why: + +```javascript +// ES Modules are already singletons! +// config.js +export const config = { + apiUrl: 'https://api.example.com', + timeout: 5000 +} + +// main.js +import { config } from './config.js' + +// other.js +import { config } from './config.js' + +// Both files get the SAME object — modules are cached! +``` + + +**Problems with Singletons:** + +1. **Testing difficulties** — Tests share the same instance, making isolation hard +2. **Hidden dependencies** — Code that uses a Singleton has an implicit dependency +3. **Tight coupling** — Components become coupled to a specific implementation +4. **ES Modules already do this** — Module exports are cached; you get the same object every time + +**Better alternatives:** Dependency injection, React Context, or simply exporting an object from a module. + + +### When Singletons Make Sense + +Despite the caveats, Singletons can be appropriate for: +- **Logging services** — One logger instance for the entire app +- **Configuration objects** — App-wide settings that shouldn't change +- **Connection pools** — Managing a single pool of database connections + + + + Detailed explanation with pros, cons, and implementation in multiple languages + + + +--- + +## The Factory Pattern + +The **Factory pattern** creates objects without exposing the creation logic. Instead of using `new` directly, you call a factory function that returns the appropriate object. This centralizes object creation and makes it easy to change how objects are created without updating every call site. + +```javascript +// Factory function — creates different user types +function createUser(type, name) { + const baseUser = { + name, + createdAt: new Date(), + greet() { + return `Hi, I'm ${this.name}` + } + } + + switch (type) { + case 'admin': + return { + ...baseUser, + role: 'admin', + permissions: ['read', 'write', 'delete', 'manage-users'], + promote(user) { + console.log(`${this.name} promoted ${user.name}`) + } + } + + case 'editor': + return { + ...baseUser, + role: 'editor', + permissions: ['read', 'write'] + } + + case 'viewer': + default: + return { + ...baseUser, + role: 'viewer', + permissions: ['read'] + } + } +} + +// Usage — no need to know the internal structure +const admin = createUser('admin', 'Alice') +const editor = createUser('editor', 'Bob') +const viewer = createUser('viewer', 'Charlie') + +console.log(admin.permissions) // ['read', 'write', 'delete', 'manage-users'] +console.log(editor.permissions) // ['read', 'write'] +console.log(viewer.greet()) // "Hi, I'm Charlie" +``` + +### When to Use the Factory Pattern + +- **Creating objects with complex setup** — Encapsulate the complexity +- **Creating different types based on input** — Switch logic in one place +- **Decoupling creation from usage** — Callers don't need to know implementation details + + +**Want to go deeper?** The Factory pattern is covered extensively in [Factories and Classes](/concepts/factories-classes), including factory functions vs classes, the `new` keyword, and when to use each approach. + + + + + Complete guide to the Factory Method pattern with diagrams and examples + + + +--- + +## The Observer Pattern + +The **Observer pattern** defines a subscription mechanism that notifies multiple objects about events. According to [Refactoring Guru](https://refactoring.guru/design-patterns/observer), it lets you "define a subscription mechanism to notify multiple objects about any events that happen to the object they're observing." + +This pattern is everywhere: DOM events, React state updates, Redux subscriptions, Node.js EventEmitter, and RxJS observables all use variations of Observer. + +### Building an Observable + +```javascript +class Observable { + constructor() { + this.observers = [] + } + + subscribe(fn) { + this.observers.push(fn) + // Return an unsubscribe function + return () => { + this.observers = this.observers.filter(observer => observer !== fn) + } + } + + notify(data) { + this.observers.forEach(observer => observer(data)) + } +} + +// Usage: A stock price tracker +const stockPrice = new Observable() + +// Subscriber 1: Log to console +const unsubscribeLogger = stockPrice.subscribe(price => { + console.log(`Stock price updated: $${price}`) +}) + +// Subscriber 2: Check for alerts +stockPrice.subscribe(price => { + if (price > 150) { + console.log('ALERT: Price above $150!') + } +}) + +// Subscriber 3: Update UI (simulated) +stockPrice.subscribe(price => { + console.log(`Updating chart with price: $${price}`) +}) + +// When price changes, all subscribers are notified +stockPrice.notify(145) +// "Stock price updated: $145" +// "Updating chart with price: $145" + +stockPrice.notify(155) +// "Stock price updated: $155" +// "ALERT: Price above $150!" +// "Updating chart with price: $155" + +// Unsubscribe the logger +unsubscribeLogger() + +stockPrice.notify(160) +// No log message, but alert and chart still update +``` + +### The Magazine Subscription Analogy + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ THE OBSERVER PATTERN │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ PUBLISHER (Observable) SUBSCRIBERS (Observers) │ +│ ────────────────────── ─────────────────────── │ +│ │ +│ ┌─────────────────────┐ ┌─────────────┐ │ +│ │ │ ──────────► │ Reader #1 │ │ +│ │ Magazine │ └─────────────┘ │ +│ │ Publisher │ ┌─────────────┐ │ +│ │ │ ──────────► │ Reader #2 │ │ +│ │ • subscribers[] │ └─────────────┘ │ +│ │ • subscribe() │ ┌─────────────┐ │ +│ │ • unsubscribe() │ ──────────► │ Reader #3 │ │ +│ │ • notify() │ └─────────────┘ │ +│ │ │ │ +│ └─────────────────────┘ │ +│ │ +│ When a new issue publishes, all subscribers receive it automatically. │ +│ Readers can subscribe or unsubscribe at any time. │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### Real-World Example: Form Validation + +```javascript +// Observable form field +class FormField { + constructor(initialValue = '') { + this.value = initialValue + this.observers = [] + } + + subscribe(fn) { + this.observers.push(fn) + return () => { + this.observers = this.observers.filter(o => o !== fn) + } + } + + setValue(newValue) { + this.value = newValue + this.observers.forEach(fn => fn(newValue)) + } +} + +// Usage +const emailField = new FormField('') + +// Validator subscriber +emailField.subscribe(value => { + const isValid = value.includes('@') + console.log(isValid ? 'Valid email' : 'Invalid email') +}) + +// Character counter subscriber +emailField.subscribe(value => { + console.log(`Characters: ${value.length}`) +}) + +emailField.setValue('test') +// "Invalid email" +// "Characters: 4" + +emailField.setValue('test@example.com') +// "Valid email" +// "Characters: 16" +``` + + + + Complete explanation with UML diagrams and pseudocode + + + +--- + +## The Proxy Pattern + +The **[Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) pattern** provides a surrogate or placeholder for another object to control access to it. In JavaScript, the ES6 `Proxy` object lets you intercept and redefine fundamental operations like property access, assignment, and function calls. + +### Basic Proxy Example + +```javascript +const user = { + name: 'Alice', + age: 25, + email: 'alice@example.com' +} + +const userProxy = new Proxy(user, { + // Intercept property reads + get(target, property) { + console.log(`Accessing property: ${property}`) + return target[property] + }, + + // Intercept property writes + set(target, property, value) { + console.log(`Setting ${property} to ${value}`) + + // Validation: age must be a non-negative number + if (property === 'age') { + if (typeof value !== 'number' || value < 0) { + throw new Error('Age must be a non-negative number') + } + } + + // Validation: email must contain @ + if (property === 'email') { + if (!value.includes('@')) { + throw new Error('Invalid email format') + } + } + + target[property] = value + return true + } +}) + +// All access goes through the proxy +console.log(userProxy.name) +// "Accessing property: name" +// "Alice" + +userProxy.age = 26 +// "Setting age to 26" + +userProxy.age = -5 +// Error: Age must be a non-negative number + +userProxy.email = 'invalid' +// Error: Invalid email format +``` + +### Practical Use Case: Lazy Loading + +```javascript +// Expensive object that we don't want to create until needed +function createExpensiveResource() { + console.log('Creating expensive resource...') + return { + data: 'Loaded data from database', + process() { + return `Processing: ${this.data}` + } + } +} + +// Proxy that delays creation until first use +function createLazyResource() { + let resource = null + + return new Proxy({}, { + get(target, property) { + // Create resource on first access + if (!resource) { + resource = createExpensiveResource() + } + + const value = resource[property] + // If it's a method, bind it to the resource + return typeof value === 'function' ? value.bind(resource) : value + } + }) +} + +const lazyResource = createLazyResource() +console.log('Proxy created, resource not loaded yet') + +// Resource is only created when we actually use it +console.log(lazyResource.data) +// "Creating expensive resource..." +// "Loaded data from database" + +console.log(lazyResource.process()) +// "Processing: Loaded data from database" +``` + +### When to Use the Proxy Pattern + +| Use Case | Example | +|----------|---------| +| **Validation** | Validate data before setting properties | +| **Logging/Debugging** | Log all property accesses for debugging | +| **Lazy initialization** | Delay expensive object creation | +| **Access control** | Restrict access to certain properties | +| **Caching** | Cache expensive computations | + + + + Complete API reference for JavaScript's Proxy object + + + Pattern explanation with diagrams and use cases + + + +--- + +## The Decorator Pattern + +The **Decorator pattern** attaches new behaviors to objects by wrapping them in objects that contain these behaviors. According to [Refactoring Guru](https://refactoring.guru/design-patterns/decorator), it lets you "attach new behaviors to objects by placing these objects inside special wrapper objects." + +In JavaScript, decorators are often implemented as functions that take an object and return an enhanced version. + +### Adding Abilities to Objects + +```javascript +// Base object +const createCharacter = (name) => ({ + name, + health: 100, + describe() { + return `${this.name} (${this.health} HP)` + } +}) + +// Decorator: Add flying ability +const withFlying = (character) => ({ + ...character, + fly() { + return `${character.name} soars through the sky!` + }, + describe() { + return `${character.describe()} [Can fly]` + } +}) + +// Decorator: Add swimming ability +const withSwimming = (character) => ({ + ...character, + swim() { + return `${character.name} dives into the water!` + }, + describe() { + return `${character.describe()} [Can swim]` + } +}) + +// Decorator: Add armor +const withArmor = (character, armorPoints) => ({ + ...character, + armor: armorPoints, + takeDamage(amount) { + const reducedDamage = Math.max(0, amount - armorPoints) + character.health -= reducedDamage + return `${character.name} takes ${reducedDamage} damage (${armorPoints} blocked)` + }, + describe() { + return `${character.describe()} [Armor: ${armorPoints}]` + } +}) + +// Compose decorators to build characters +const duck = withSwimming(withFlying(createCharacter('Duck'))) +console.log(duck.describe()) // "Duck (100 HP) [Can fly] [Can swim]" +console.log(duck.fly()) // "Duck soars through the sky!" +console.log(duck.swim()) // "Duck dives into the water!" + +const knight = withArmor(createCharacter('Knight'), 20) +console.log(knight.describe()) // "Knight (100 HP) [Armor: 20]" +console.log(knight.takeDamage(50)) // "Knight takes 30 damage (20 blocked)" +``` + +### Function Decorators + +Decorators also work great with functions: + +```javascript +// Decorator: Log function calls +const withLogging = (fn, fnName) => { + return function(...args) { + console.log(`Calling ${fnName} with:`, args) + const result = fn.apply(this, args) + console.log(`${fnName} returned:`, result) + return result + } +} + +// Decorator: Memoize (cache) results +const withMemoization = (fn) => { + const cache = new Map() + + return function(...args) { + const key = JSON.stringify(args) + + if (cache.has(key)) { + console.log('Cache hit!') + return cache.get(key) + } + + const result = fn.apply(this, args) + cache.set(key, result) + return result + } +} + +// Original function +function fibonacci(n) { + if (n <= 1) return n + return fibonacci(n - 1) + fibonacci(n - 2) +} + +// Decorated version with logging +const loggedAdd = withLogging((a, b) => a + b, 'add') +loggedAdd(2, 3) +// "Calling add with: [2, 3]" +// "add returned: 5" + +// Decorated fibonacci with memoization +const memoizedFib = withMemoization(function fib(n) { + if (n <= 1) return n + return memoizedFib(n - 1) + memoizedFib(n - 2) +}) + +console.log(memoizedFib(10)) // 55 +console.log(memoizedFib(10)) // "Cache hit!" — 55 +``` + +### When to Use the Decorator Pattern + +- **Adding features without modifying original code** — Open/Closed Principle +- **Composing behaviors dynamically** — Mix and match capabilities +- **Cross-cutting concerns** — Logging, caching, validation, timing + + + + Full explanation with structure diagrams and applicability guidelines + + + +--- + +## Common Mistakes with Design Patterns + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ DESIGN PATTERN MISTAKES │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ MISTAKE #1: PATTERN OVERUSE │ +│ ─────────────────────────── │ +│ Using patterns where simple code would work better. │ +│ A plain function is often better than a Factory class. │ +│ │ +│ MISTAKE #2: WRONG PATTERN CHOICE │ +│ ───────────────────────────── │ +│ Using Singleton when you just need a module export. │ +│ Using Observer when a simple callback would suffice. │ +│ │ +│ MISTAKE #3: IGNORING JAVASCRIPT IDIOMS │ +│ ──────────────────────────────────── │ +│ JavaScript has closures, first-class functions, and ES modules. │ +│ Many classical patterns simplify dramatically in JavaScript. │ +│ │ +│ MISTAKE #4: PREMATURE ABSTRACTION │ +│ ──────────────────────────────── │ +│ Adding patterns before you have a real problem to solve. │ +│ "You Ain't Gonna Need It" (YAGNI) applies to patterns too. │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### The "Golden Hammer" Anti-Pattern + +When you learn a new pattern, resist the urge to use it everywhere: + +```javascript +// ❌ OVERKILL: Factory for simple objects +class UserFactory { + createUser(name) { + return new User(name) + } +} +const factory = new UserFactory() +const user = factory.createUser('Alice') + +// ✓ SIMPLE: Just create the object +const user = { name: 'Alice' } +// or +const user = new User('Alice') +``` + + +**Ask yourself these questions before using a pattern:** + +1. **Do I have a real problem?** Don't solve problems you don't have yet. +2. **Is there a simpler solution?** A plain function or object might be enough. +3. **Does JavaScript already solve this?** ES modules, Promises, and iterators are built-in patterns. +4. **Will my team understand it?** Patterns only help if everyone knows them. + + +--- + +## Choosing the Right Pattern + +| Problem | Pattern | Alternative | +|---------|---------|-------------| +| Need to organize code with private state | **Module** | ES6 module exports | +| Need exactly one instance | **Singleton** | Just export an object from a module | +| Need to create objects dynamically | **Factory** | Plain function returning objects | +| Need to notify multiple listeners of changes | **Observer** | EventEmitter, callbacks, or a library | +| Need to control or validate object access | **Proxy** | Getter/setter methods | +| Need to add behavior without modification | **Decorator** | Higher-order functions, composition | + + +**Rule of Thumb:** Start with the simplest solution that works. Introduce patterns when you hit a real problem they solve, not before. + + +--- + +## Key Takeaways + + +**The key things to remember:** + +1. **Design patterns are templates, not code** — Adapt them to your specific problem; don't force-fit them + +2. **JavaScript simplifies many patterns** — First-class functions, closures, and ES modules reduce boilerplate + +3. **Module pattern organizes code** — Use ES modules for new projects; understand IIFE pattern for legacy code + +4. **Singleton is often unnecessary** — ES module exports are already cached; use sparingly if at all + +5. **Factory centralizes object creation** — Great for creating different types based on input + +6. **Observer enables event-driven code** — The foundation of DOM events, React state, and reactive programming + +7. **Proxy intercepts object operations** — Use for validation, logging, lazy loading, and access control + +8. **Decorator adds behavior through wrapping** — Compose features without modifying original code + +9. **Avoid pattern overuse** — Simple code beats clever patterns; apply the YAGNI principle + +10. **Learn to recognize patterns in the wild** — DOM events use Observer, Promises use a form of Observer, middleware uses Decorator + + +--- + +## Test Your Knowledge + + + + **Answer:** + + The Module pattern encapsulates code into reusable units with **private and public parts**. It allows you to: + - Hide implementation details (private variables and functions) + - Expose only a public API + - Avoid polluting the global namespace + + In modern JavaScript, ES6 modules (`import`/`export`) provide this naturally. Variables in a module are private unless exported. + + ```javascript + // privateHelper is not exported — it's private + function privateHelper() { /* ... */ } + + // Only publicFunction is accessible to importers + export function publicFunction() { + privateHelper() + } + ``` + + + + **Answer:** + + Singleton is often unnecessary in JavaScript because: + + 1. **ES modules are already singletons** — When you export an object, all importers get the same instance + 2. **Testing difficulties** — Tests share state, making isolation hard + 3. **Hidden dependencies** — Code using Singletons has implicit dependencies + 4. **JavaScript can create objects directly** — No need for the class-based workarounds other languages require + + ```javascript + // ES module — already a singleton! + export const config = { apiUrl: '...' } + + // Every import gets the same object + import { config } from './config.js' // Same instance everywhere + ``` + + + + **Answer:** + + The Observer pattern has three key parts: + + 1. **Subscriber list** — An array to store observer functions + 2. **Subscribe method** — Adds a function to the list (often returns an unsubscribe function) + 3. **Notify method** — Calls all subscribed functions with data + + ```javascript + class Observable { + constructor() { + this.observers = [] // 1. Subscriber list + } + + subscribe(fn) { // 2. Subscribe method + this.observers.push(fn) + return () => { // Returns unsubscribe + this.observers = this.observers.filter(o => o !== fn) + } + } + + notify(data) { // 3. Notify method + this.observers.forEach(fn => fn(data)) + } + } + ``` + + + + **Answer:** + + Both wrap objects, but they have different purposes: + + **Proxy Pattern:** + - **Controls access** to an object + - Intercepts operations like get, set, delete + - The proxy typically has the same interface as the target + - Use for: validation, logging, lazy loading, access control + + **Decorator Pattern:** + - **Adds new behavior** to an object + - Wraps the object and extends its capabilities + - May add new methods or modify existing ones + - Use for: composing features, cross-cutting concerns + + ```javascript + // Proxy — same interface, controlled access + const proxy = new Proxy(obj, { get(t, p) { /* intercept */ } }) + + // Decorator — enhanced interface, new behavior + const enhanced = withLogging(withCache(obj)) + ``` + + + + **Answer:** + + Use the Factory pattern when: + + 1. **Object creation is complex** — Encapsulate setup logic in one place + 2. **You need different types based on input** — Switch logic centralized in the factory + 3. **You want to decouple creation from usage** — Callers don't need to know implementation + 4. **You might change how objects are created** — Update the factory, not every call site + + ```javascript + // Factory — creation logic in one place + function createNotification(type, message) { + switch (type) { + case 'error': return { type, message, color: 'red' } + case 'success': return { type, message, color: 'green' } + default: return { type: 'info', message, color: 'blue' } + } + } + + // Easy to use — no need to know the structure + const notification = createNotification('error', 'Something went wrong') + ``` + + + + **Answer:** + + The "Golden Hammer" anti-pattern is the tendency to use a familiar tool (or pattern) for every problem, even when it's not appropriate. + + **Signs you're doing this:** + - Using Singleton for everything that "should be global" + - Creating Factory classes for simple object literals + - Using Observer when a callback would suffice + - Adding patterns before you have a real problem + + **How to avoid it:** + - Start with the simplest solution + - Add patterns only when you hit a real problem they solve + - Ask: "Would a plain function/object work here?" + - Remember: Code clarity beats clever patterns + + + +--- + +## Related Concepts + + + + Deep dive into object creation with factory functions and ES6 classes + + + How JavaScript evolved from IIFEs to modern ES modules + + + Functions that work with functions — the foundation of many patterns + + + How closures enable private state in the Module pattern + + + +--- + +## Reference + + + + Complete API reference for JavaScript's built-in Proxy object + + + Official guide to ES6 modules with import and export + + + Complete catalog of classic design patterns with examples + + + The Reflect object used with Proxy for default behavior + + + +## Articles + + + + Lydia Hallie and Addy Osmani's modern guide with animated visualizations. Each pattern gets its own interactive explanation showing exactly how data flows. + + + Beginner-friendly walkthrough of essential patterns with practical code examples. Great starting point if you're new to design patterns. + + + Addy Osmani's free online book, updated for modern JavaScript. The definitive resource covering patterns, anti-patterns, and real-world applications. + + + Authoritative guide on adding behaviors to classes without inheritance. Shows the EventMixin pattern that's used throughout JavaScript libraries. + + + +## Videos + + + + Fireship's fast-paced overview covering the essential patterns every developer should know. Perfect for a quick refresher or introduction. + + + Free comprehensive course covering MVC, MVP, and organizing large JavaScript applications. Great for understanding patterns in context. + + + Fun Fun Function's engaging explanation of factories vs classes. MPJ's conversational style makes complex concepts approachable. + + diff --git a/docs/concepts/dom.mdx b/docs/concepts/dom.mdx new file mode 100644 index 00000000..83fe1a5f --- /dev/null +++ b/docs/concepts/dom.mdx @@ -0,0 +1,2157 @@ +--- +title: "DOM: How Browsers Represent Web Pages in JavaScript" +sidebarTitle: "DOM: How Browsers Represent Web Pages" +description: "Learn how the DOM works in JavaScript. Understand how browsers represent HTML as a tree, select and manipulate elements, traverse nodes, and optimize rendering performance." +--- + +How does JavaScript change what you see on a webpage? How do you click a button and see new content appear, or type in a form and watch suggestions pop up? How does a "dark mode" toggle instantly transform an entire page? + +```javascript +// The DOM lets you do things like this: +document.querySelector('h1').textContent = 'Hello, DOM!' +document.body.style.backgroundColor = 'lightblue' +document.getElementById('btn').addEventListener('click', handleClick) +``` + +The **[Document Object Model (DOM)](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model)** is the bridge between your HTML and JavaScript. It lets you read, modify, and respond to changes in web page content. With the DOM, you can use methods like **[`querySelector()`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)** to find elements, **[`getElementById()`](https://developer.mozilla.org/en-US/docs/Web/API/Document/getElementById)** to grab specific nodes, and **[`addEventListener()`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener)** to respond to user interactions. + + +**What you'll learn in this guide:** +- What the DOM is in JavaScript and how it differs from HTML +- How to select DOM elements (getElementById vs querySelector) +- How to traverse the DOM tree (parent, children, siblings) +- How to manipulate DOM elements (create, modify, remove) +- The difference between properties and attributes +- How the browser turns DOM → pixels (the Critical Rendering Path) +- Performance best practices (avoid layout thrashing!) + + + +**Prerequisite:** This guide assumes basic familiarity with [HTML](https://developer.mozilla.org/en-US/docs/Learn/HTML/Introduction_to_HTML) and [CSS](https://developer.mozilla.org/en-US/docs/Learn/CSS/First_steps). If you're new to web development, start there first! + + +--- + +## What is the DOM in JavaScript? + +The **[Document Object Model (DOM)](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model)** is a programming interface that represents HTML documents as a tree of objects. When a browser loads a webpage, it parses the HTML and creates the DOM, a live, structured representation that JavaScript can read and modify. Every element, attribute, and piece of text becomes a node in this tree. **In short: the DOM is how JavaScript "sees" and changes a webpage.** + +--- + +## How the DOM Tree Structure Works + +Think of the DOM like a family tree. At the top sits `document` (the family historian who knows everyone). Below it is `` (the matriarch), which has two children: `` and ``. Each of these has their own children, grandchildren, and so on. + +``` + THE DOM FAMILY TREE + + ┌──────────┐ + │ document │ ← The family historian + │ (root) │ (knows everyone!) + └────┬─────┘ + │ + ┌────┴─────┐ + │ │ ← Great-grandma + └────┬─────┘ (the matriarch) + ┌─────────────┴─────────────┐ + │ │ + ┌────┴────┐ ┌────┴────┐ + │ │ │ │ ← The two branches + └────┬────┘ └────┬────┘ of the family + │ │ + ┌──────┴──────┐ ┌──────────┼──────────┐ + │ │ │ │ │ + ┌────┴────┐ ┌────┴────┐ ┌───┴───┐ ┌────┴────┐ ┌───┴───┐ + │ │ │ <meta> │ │ <nav> │ │ <main> │ │<footer>│ + └────┬────┘ └─────────┘ └───┬───┘ └────┬────┘ └───────┘ + │ │ │ + "My Page" ┌────┴────┐ ┌──┴──┐ + (text) │ <ul> │ │<div>│ ← Cousins + └────┬────┘ └──┬──┘ + │ │ + ┌────┼────┐ ... + │ │ │ + <li> <li> <li> ← Siblings +``` + +Just like navigating a family reunion, the DOM lets you: + +| Action | Family Analogy | DOM Method | +|--------|----------------|------------| +| Find your parent | "Who's your mom?" | `element.parentNode` | +| Find your kids | "Where are your children?" | `element.children` | +| Find your sibling | "Who's your brother?" | `element.nextElementSibling` | +| Search the whole family | "Where's cousin Bob?" | `document.querySelector('#bob')` | + +<Note> +**Key insight:** Every element, text, and comment in your HTML becomes a "node" in this tree. JavaScript lets you navigate this tree and modify it: changing content, adding elements, or removing them entirely. +</Note> + +--- + +## What the DOM is NOT + +### The DOM is NOT Your HTML Source Code + +Here's the key thing: your HTML file and the DOM are **different things**: + +<Tabs> + <Tab title="HTML Source"> + ```html + <!-- What you wrote (invalid HTML - missing head/body) --> + <!DOCTYPE html> + <html> + Hello, World! + </html> + ``` + </Tab> + <Tab title="Resulting DOM"> + ```html + <!-- What the browser creates (fixed!) --> + <!DOCTYPE html> + <html> + <head></head> + <body> + Hello, World! + </body> + </html> + ``` + </Tab> +</Tabs> + +The browser **fixes your mistakes**! It adds missing `<head>` and `<body>` tags, closes unclosed tags, and corrects nesting errors. The DOM is the corrected version. + +### The DOM is NOT What You See in DevTools (Exactly) + +DevTools shows you something close to the DOM, but it also shows **CSS pseudo-elements** (`::before`, `::after`) which are NOT part of the DOM: + +```css +/* This creates visual content, but NOT DOM nodes */ +.quote::before { + content: '"'; +} +``` + +Pseudo-elements exist in the **render tree** (for display), but not in the DOM (for JavaScript). You can't select them with `querySelector`! + +### The DOM is NOT the Render Tree + +The **Render Tree** is what actually gets painted to the screen. It excludes: + +```html +<!-- These are in the DOM but NOT in the Render Tree --> +<head>...</head> <!-- Never rendered --> +<script>...</script> <!-- Never rendered --> +<div style="display: none">Hidden</div> <!-- Excluded from render --> +``` + +``` +DOM Render Tree +┌─────────────────────┐ ┌─────────────────────┐ +│ <html> │ │ <html> │ +│ <head> │ │ <body> │ +│ <title> │ │ <h1> │ +│ <body> │ │ "Hello" │ +│ <h1>Hello</h1> │ │ <p> │ +│ <p>World</p> │ │ "World" │ +│ <div hidden> │ │ │ +│ Secret! │ │ (no hidden div!) │ +│ </div> │ │ │ +└─────────────────────┘ └─────────────────────┘ +``` + +### The `document` Object: Your Entry Point + +The **[`document`](https://developer.mozilla.org/en-US/docs/Web/API/Document)** object is your gateway to the DOM. It's automatically available in any browser JavaScript. Key properties include **[`document.documentElement`](https://developer.mozilla.org/en-US/docs/Web/API/Document/documentElement)** (the root `<html>` element), **[`document.head`](https://developer.mozilla.org/en-US/docs/Web/API/Document/head)**, **[`document.body`](https://developer.mozilla.org/en-US/docs/Web/API/Document/body)**, and **[`document.title`](https://developer.mozilla.org/en-US/docs/Web/API/Document/title)**: + +```javascript +// document is the root of everything +console.log(document) // The entire document +console.log(document.documentElement) // <html> element +console.log(document.head) // <head> element +console.log(document.body) // <body> element +console.log(document.title) // Page title (getter/setter!) + +// You can modify the document +document.title = 'New Title' // Changes browser tab title +``` + +--- + +## DOM Node Types Explained + +Everything in the DOM is a **[Node](https://developer.mozilla.org/en-US/docs/Web/API/Node)**. But not all nodes are created equal! + +### The Node Type Hierarchy + +``` + Node (base class) + │ + ┌─────────────────────┼─────────────────────┐ + │ │ │ + Document Element CharacterData + │ │ │ + HTMLDocument ┌────┴────┐ ┌─────┴─────┐ + │ │ │ │ + HTMLElement SVGElement Text Comment + │ + ┌────────────────┼────────────────┐ + │ │ │ + HTMLDivElement HTMLSpanElement HTMLInputElement + ... +``` + +### Node Types You'll Encounter + +| Node Type | `nodeType` | `nodeName` | Example | +|-----------|------------|------------|---------| +| [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) | `1` | Tag name (uppercase) | `<div>`, `<p>`, `<span>` | +| [Text](https://developer.mozilla.org/en-US/docs/Web/API/Text) | `3` | `#text` | Text inside elements | +| [Comment](https://developer.mozilla.org/en-US/docs/Web/API/Comment) | `8` | `#comment` | `<!-- comment -->` | +| [Document](https://developer.mozilla.org/en-US/docs/Web/API/Document) | `9` | `#document` | The `document` object | +| [DocumentFragment](https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment) | `11` | `#document-fragment` | Virtual container | + +```javascript +const div = document.createElement('div') +console.log(div.nodeType) // 1 (Element) +console.log(div.nodeName) // "DIV" + +const text = document.createTextNode('Hello') +console.log(text.nodeType) // 3 (Text) +console.log(text.nodeName) // "#text" + +console.log(document.nodeType) // 9 (Document) +console.log(document.nodeName) // "#document" +``` + +The **[`createElement()`](https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement)** and **[`createTextNode()`](https://developer.mozilla.org/en-US/docs/Web/API/Document/createTextNode)** methods create new nodes that you can add to the DOM. + +### Node Type Constants + +Instead of remembering numbers, use the constants: + +```javascript +Node.ELEMENT_NODE // 1 +Node.TEXT_NODE // 3 +Node.COMMENT_NODE // 8 +Node.DOCUMENT_NODE // 9 +Node.DOCUMENT_FRAGMENT_NODE // 11 + +// Check if something is an element +if (node.nodeType === Node.ELEMENT_NODE) { + console.log('This is an element!') +} +``` + +### Visualizing a Real DOM Tree + +Given this HTML: + +```html +<div id="container"> + <h1>Title</h1> + <!-- A comment --> + <p>Paragraph</p> +</div> +``` + +The actual DOM tree looks like this (including text nodes from whitespace!): + +``` +div#container +├── #text (newline + spaces) +├── h1 +│ └── #text "Title" +├── #text (newline + spaces) +├── #comment " A comment " +├── #text (newline + spaces) +├── p +│ └── #text "Paragraph" +└── #text (newline) +``` + +<Warning> +**The Whitespace Gotcha!** Line breaks and spaces between HTML tags create **text nodes**. This surprises many developers! We'll see how to handle this in the traversal section. +</Warning> + +--- + +## How to Select DOM Elements + +Before you can manipulate an element, you need to find it. JavaScript provides several methods through the **[`document`](https://developer.mozilla.org/en-US/docs/Web/API/Document)** object: + +### The getElementById() Classic + +The **[`getElementById()`](https://developer.mozilla.org/en-US/docs/Web/API/Document/getElementById)** method is the fastest way to select a single element by its unique ID: + +```javascript +// HTML: <div id="hero">Welcome!</div> + +const hero = document.getElementById('hero') +console.log(hero) // <div id="hero">Welcome!</div> +console.log(hero.id) // "hero" +console.log(hero.textContent) // "Welcome!" + +// Returns null if not found (not an error!) +const ghost = document.getElementById('nonexistent') +console.log(ghost) // null +``` + +<Tip> +IDs must be unique in a document. If you have duplicate IDs, `getElementById` returns the first one. But don't do this. It's invalid HTML! +</Tip> + +### getElementsByClassName() and getElementsByTagName() + +**[`getElementsByClassName()`](https://developer.mozilla.org/en-US/docs/Web/API/Document/getElementsByClassName)** and **[`getElementsByTagName()`](https://developer.mozilla.org/en-US/docs/Web/API/Document/getElementsByTagName)** select multiple elements by class or tag name: + +```javascript +// HTML: +// <p class="intro">First</p> +// <p class="intro">Second</p> +// <p>Third</p> + +const intros = document.getElementsByClassName('intro') +console.log(intros.length) // 2 +console.log(intros[0]) // <p class="intro">First</p> +console.log(intros[0].textContent) // "First" + +const allParagraphs = document.getElementsByTagName('p') +console.log(allParagraphs.length) // 3 +``` + +### The Modern Way: querySelector() and querySelectorAll() + +**[`querySelector()`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)** and **[`querySelectorAll()`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelectorAll)** use CSS selectors to find elements. Much more powerful! + +```javascript +// querySelector returns the FIRST match (or null) +const firstButton = document.querySelector('button') // First <button> element +const submitBtn = document.querySelector('#submit') // Element with id="submit" +const firstCard = document.querySelector('.card') // First element with class="card" +const navLink = document.querySelector('nav a.active') // <a class="active"> inside <nav> +const dataItem = document.querySelector('[data-id="123"]') // Element with data-id="123" + +// querySelectorAll returns ALL matches (NodeList) +const allButtons = document.querySelectorAll('button') // All <button> elements +const allCards = document.querySelectorAll('.card') // All elements with class="card" +const evenRows = document.querySelectorAll('tr:nth-child(even)') // Every even table row +``` + +### Selector Examples + +```javascript +// By ID +document.querySelector('#main') + +// By class +document.querySelector('.active') +document.querySelectorAll('.btn.primary') + +// By tag +document.querySelector('header') +document.querySelectorAll('li') + +// By attribute +document.querySelector('[type="submit"]') +document.querySelector('[data-modal="login"]') + +// Descendant selectors +document.querySelector('nav ul li a') +document.querySelector('.sidebar .widget:first-child') + +// Pseudo-selectors (limited support) +document.querySelectorAll('input:not([type="hidden"])') +document.querySelector('p:first-of-type') +``` + +### Live vs Static Collections + +This difference trips up many developers. **[`getElementsByClassName()`](https://developer.mozilla.org/en-US/docs/Web/API/Document/getElementsByClassName)** returns a live **[HTMLCollection](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCollection)**, while **[`querySelectorAll()`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelectorAll)** returns a static **[NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList)**: + +```javascript +const liveList = document.getElementsByClassName('item') // LIVE HTMLCollection +const staticList = document.querySelectorAll('.item') // STATIC NodeList + +// Start with 3 items +console.log(liveList.length) // 3 +console.log(staticList.length) // 3 + +// Add a new item to the DOM +const newItem = document.createElement('div') +newItem.className = 'item' +document.body.appendChild(newItem) + +// Check lengths again +console.log(liveList.length) // 4 (automatically updated!) +console.log(staticList.length) // 3 (still the old snapshot) +``` + +| Method | Returns | Live? | +|--------|---------|-------| +| `getElementById()` | Element or null | N/A | +| `getElementsByClassName()` | HTMLCollection | **Yes** (live) | +| `getElementsByTagName()` | HTMLCollection | **Yes** (live) | +| `querySelector()` | Element or null | N/A | +| `querySelectorAll()` | NodeList | **No** (static) | + +### Scoped Selection + +You can call selection methods on any element, not just `document`: + +```javascript +const nav = document.querySelector('nav') + +// Find links ONLY inside nav +const navLinks = nav.querySelectorAll('a') + +// Find the active link inside nav +const activeLink = nav.querySelector('.active') +``` + +This is faster than searching the entire document and helps avoid selecting unintended elements. + +### Performance Comparison + +<AccordionGroup> + <Accordion title="Which selector method is fastest?"> + In order of speed (fastest first): + + 1. **`getElementById()`** - Direct hashtable lookup, O(1) + 2. **`getElementsByClassName()`** - Optimized internal lookup + 3. **`getElementsByTagName()`** - Optimized internal lookup + 4. **`querySelector()`** - Must parse CSS selector + 5. **`querySelectorAll()`** - Must parse and find all matches + + However, for most applications, **the difference is negligible**. Use `querySelector/querySelectorAll` for readability unless you're selecting thousands of elements in a loop. + + ```javascript + // Premature optimization - don't do this + const el1 = document.getElementById('myId') + + // This is fine and more readable + const el2 = document.querySelector('#myId') + ``` + </Accordion> +</AccordionGroup> + +--- + +## How to Traverse the DOM + +Once you have an element, you can navigate to related elements without querying the entire document. + +### Traversing Downwards (To Children) + +```javascript +const ul = document.querySelector('ul') + +// Get ALL child nodes (including text nodes!) +const allChildNodes = ul.childNodes // NodeList + +// Get only ELEMENT children (usually what you want) +const elementChildren = ul.children // HTMLCollection + +// Get specific children +const firstChild = ul.firstChild // First node (might be text!) +const firstElement = ul.firstElementChild // First ELEMENT child +const lastChild = ul.lastChild // Last node +const lastElement = ul.lastElementChild // Last ELEMENT child +``` + +<Warning> +**The Text Node Trap!** Look at this HTML: + +```html +<ul> + <li>One</li> + <li>Two</li> +</ul> +``` + +What is `ul.firstChild`? It's NOT the first `<li>`! It's a **text node** containing the newline and spaces after `<ul>`. Use `firstElementChild` to get the actual `<li>` element. +</Warning> + +### Traversing Upwards (To Parents) + +```javascript +const li = document.querySelector('li') + +// Direct parent +const parent = li.parentNode // Usually same as parentElement +const parentEl = li.parentElement // Guaranteed to be an Element (or null) + +// Find ancestor matching selector (very useful!) +const form = li.closest('form') // Finds nearest ancestor <form> +const card = li.closest('.card') // Finds nearest ancestor with class "card" + +// closest() includes the element itself +const self = li.closest('li') // Returns li itself if it matches! +``` + +The **[`closest()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/closest)** method is useful for event delegation (see [Event Loop](/concepts/event-loop) for how events are processed): + +```javascript +// Handle clicks on any button inside a card +document.addEventListener('click', (e) => { + const card = e.target.closest('.card') + if (card) { + console.log('Clicked inside card:', card) + } +}) +``` + +### Traversing Sideways (To Siblings) + +```javascript +const secondLi = document.querySelectorAll('li')[1] + +// Previous/next nodes (might be text!) +const prevNode = secondLi.previousSibling +const nextNode = secondLi.nextSibling + +// Previous/next ELEMENTS (usually what you want) +const prevElement = secondLi.previousElementSibling +const nextElement = secondLi.nextElementSibling + +// Returns null at the boundaries +const firstLi = document.querySelector('li') +console.log(firstLi.previousElementSibling) // null (no previous sibling) +``` + +### Node vs Element Properties Cheat Sheet + +| Get... | Node Property (includes text) | Element Property (elements only) | +|--------|-------------------------------|----------------------------------| +| Parent | `parentNode` | `parentElement` | +| Children | `childNodes` | `children` | +| First child | `firstChild` | `firstElementChild` | +| Last child | `lastChild` | `lastElementChild` | +| Previous sibling | `previousSibling` | `previousElementSibling` | +| Next sibling | `nextSibling` | `nextElementSibling` | + +<Tip> +**Rule of thumb:** Unless you specifically need text nodes, always use the Element variants (`children`, `firstElementChild`, `nextElementSibling`, etc.) +</Tip> + +### Practical Example: Building a Breadcrumb Trail + +```javascript +// Get all ancestors of an element +function getAncestors(element) { + const ancestors = [] + let current = element.parentElement + + while (current && current !== document.body) { + ancestors.push(current) + current = current.parentElement + } + + return ancestors +} + +const deepElement = document.querySelector('.deeply-nested') +console.log(getAncestors(deepElement)) +// [<div.parent>, <section>, <main>, ...] +``` + +--- + +## Creating and Manipulating Elements + +The real power of the DOM is the ability to create, modify, and remove elements dynamically. + +### Creating Elements + +Use **[`createElement()`](https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement)** to create new elements and **[`createTextNode()`](https://developer.mozilla.org/en-US/docs/Web/API/Document/createTextNode)** to create text nodes: + +```javascript +// Create a new element +const div = document.createElement('div') +const span = document.createElement('span') +const img = document.createElement('img') + +// Create a text node +const text = document.createTextNode('Hello, world!') + +// Create a comment node +const comment = document.createComment('This is a comment') + +// Elements are created "detached" - not yet in the DOM! +console.log(div.parentNode) // null +``` + +### Adding Elements to the DOM + +There are many ways to add elements. Here's a comprehensive overview using methods like **[`appendChild()`](https://developer.mozilla.org/en-US/docs/Web/API/Node/appendChild)**, **[`insertBefore()`](https://developer.mozilla.org/en-US/docs/Web/API/Node/insertBefore)**, **[`append()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/append)**, and **[`prepend()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/prepend)**: + +<Tabs> + <Tab title="appendChild()"> + Adds a node as the **last child** of a parent: + + ```javascript + const ul = document.querySelector('ul') + const li = document.createElement('li') + li.textContent = 'New item' + + ul.appendChild(li) + // <ul> + // <li>Existing</li> + // <li>New item</li> ← Added at the end + // </ul> + ``` + </Tab> + <Tab title="insertBefore()"> + Inserts a node **before** a reference node: + + ```javascript + const ul = document.querySelector('ul') + const existingLi = ul.querySelector('li') + const newLi = document.createElement('li') + newLi.textContent = 'First!' + + ul.insertBefore(newLi, existingLi) + // <ul> + // <li>First!</li> ← Inserted before + // <li>Existing</li> + // </ul> + ``` + </Tab> + <Tab title="append() / prepend()"> + Modern methods that accept multiple nodes AND strings: + + ```javascript + const div = document.querySelector('div') + + // append() - adds to the END + div.append('Text', document.createElement('span'), 'More text') + + // prepend() - adds to the START + div.prepend(document.createElement('strong')) + ``` + </Tab> + <Tab title="before() / after()"> + Insert as siblings (not children): + + ```javascript + const h1 = document.querySelector('h1') + + // Insert BEFORE h1 (as previous sibling) + h1.before(document.createElement('nav')) + + // Insert AFTER h1 (as next sibling) + h1.after(document.createElement('p')) + ``` + </Tab> +</Tabs> + +### insertAdjacentHTML() - The Swiss Army Knife + +For inserting HTML strings, **[`insertAdjacentHTML()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML)** is powerful and fast: + +```javascript +const div = document.querySelector('div') + +// Four positions to insert: +div.insertAdjacentHTML('beforebegin', '<p>Before div</p>') +div.insertAdjacentHTML('afterbegin', '<p>First child of div</p>') +div.insertAdjacentHTML('beforeend', '<p>Last child of div</p>') +div.insertAdjacentHTML('afterend', '<p>After div</p>') +``` + +Visual representation: + +```html +<!-- beforebegin --> +<div> + <!-- afterbegin --> + existing content + <!-- beforeend --> +</div> +<!-- afterend --> +``` + +### Removing Elements + +<Tabs> + <Tab title="remove()"> + Modern and simple. Element removes itself: + + ```javascript + const element = document.querySelector('.to-remove') + element.remove() // Gone! + ``` + </Tab> + <Tab title="removeChild()"> + Classic method. Remove via parent: + + ```javascript + const parent = document.querySelector('ul') + const child = parent.querySelector('li') + parent.removeChild(child) + + // Or remove from any element + element.parentNode.removeChild(element) + ``` + </Tab> +</Tabs> + +### Cloning Elements + +Use **[`cloneNode()`](https://developer.mozilla.org/en-US/docs/Web/API/Node/cloneNode)** to duplicate elements: + +```javascript +const original = document.querySelector('.card') + +// Shallow clone (element only, no children) +const shallow = original.cloneNode(false) + +// Deep clone (element AND all descendants) +const deep = original.cloneNode(true) + +// Clones are detached - must add to DOM +document.body.appendChild(deep) +``` + +<Warning> +**ID Collision!** If you clone an element with an ID, you'll have duplicate IDs in your document (invalid HTML). Remove or change the ID after cloning: + +```javascript +const clone = original.cloneNode(true) +clone.id = '' // Remove ID +// or +clone.id = 'new-unique-id' +``` +</Warning> + +### DocumentFragment - Batch Operations + +When adding many elements, using a **[`DocumentFragment`](https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment)** is more efficient: + +```javascript +// Bad: Multiple DOM updates (potentially multiple reflows) +const ul = document.querySelector('ul') +for (let i = 0; i < 1000; i++) { + const li = document.createElement('li') + li.textContent = `Item ${i}` + ul.appendChild(li) // Modifies live DOM each iteration +} + +// Good: Single DOM update +const ul = document.querySelector('ul') +const fragment = document.createDocumentFragment() + +for (let i = 0; i < 1000; i++) { + const li = document.createElement('li') + li.textContent = `Item ${i}` + fragment.appendChild(li) // No DOM update (fragment is detached) +} + +ul.appendChild(fragment) // Single DOM update! +``` + +A `DocumentFragment` is a lightweight container that: +- Is not part of the DOM tree +- Has no parent +- When appended, only its **children** are inserted (the fragment itself disappears) + +<Note> +**Modern browser optimization:** Browsers may batch consecutive DOM modifications and perform a single reflow. However, using DocumentFragment is still the recommended pattern because it's explicit, works consistently across all browsers, and avoids any risk of forced synchronous layouts if you read layout properties between writes. +</Note> + +--- + +## Modifying Content + +Three properties let you read and write element content: **[`innerHTML`](https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML)**, **[`textContent`](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent)**, and **[`innerText`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/innerText)**. + +### innerHTML - Parse and Insert HTML + +```javascript +const div = document.querySelector('div') + +// Read HTML content +console.log(div.innerHTML) // "<p>Hello</p><span>World</span>" + +// Write HTML content (parses the string!) +div.innerHTML = '<h1>New Title</h1><p>New paragraph</p>' + +// Clear all content +div.innerHTML = '' +``` + +<Warning> +**Security Alert: XSS Vulnerability!** + +Never use `innerHTML` with user-provided content: + +```javascript +// DANGEROUS! User could inject: <img src=x onerror="stealCookies()"> +div.innerHTML = userInput // NO! + +// Safe alternatives: +div.textContent = userInput // Escapes HTML +// or sanitize the input first +``` +</Warning> + +### textContent - Plain Text Only + +```javascript +const div = document.querySelector('div') + +// Read text (ignores HTML tags) +// <div><p>Hello</p><span>World</span></div> +console.log(div.textContent) // "HelloWorld" + +// Write text (HTML is escaped, not parsed) +div.textContent = '<script>alert("XSS")</script>' +// Displays literally: <script>alert("XSS")</script> +// Safe from XSS! +``` + +### innerText - Rendered Text + +```javascript +const div = document.querySelector('div') + +// innerText respects CSS visibility +// <div>Hello <span style="display:none">Hidden</span> World</div> + +console.log(div.textContent) // "Hello Hidden World" +console.log(div.innerText) // "Hello World" (Hidden is excluded!) +``` + +### When to Use Each + +| Property | Use Case | +|----------|----------| +| `innerHTML` | Inserting trusted HTML (never user input!) | +| `textContent` | Setting/getting plain text (safe, fast) | +| `innerText` | Getting text as user sees it (slower, respects CSS) | + +```javascript +// Performance: textContent is faster than innerText +// because innerText must calculate styles + +// Setting text content (both work, textContent is faster) +element.textContent = 'Hello' // Preferred +element.innerText = 'Hello' // Works but slower +``` + +--- + +## How to Work with DOM Attributes + +HTML elements have attributes. JavaScript lets you read, write, and remove them using **[`getAttribute()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute)**, **[`setAttribute()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute)**, **[`hasAttribute()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/hasAttribute)**, and **[`removeAttribute()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/removeAttribute)**. + +### Standard Attribute Methods + +```javascript +const link = document.querySelector('a') + +// Get attribute value +const href = link.getAttribute('href') +const target = link.getAttribute('target') + +// Set attribute value +link.setAttribute('href', 'https://example.com') +link.setAttribute('target', '_blank') + +// Check if attribute exists +if (link.hasAttribute('target')) { + console.log('Link opens in new tab') +} + +// Remove attribute +link.removeAttribute('target') +``` + +### Properties vs Attributes: The Difference + +This confuses many developers! **Attributes** are in the HTML. **Properties** are on the DOM object. + +```html +<input type="text" value="initial"> +``` + +```javascript +const input = document.querySelector('input') + +// ATTRIBUTE: The original HTML value +console.log(input.getAttribute('value')) // "initial" + +// PROPERTY: The current state +console.log(input.value) // "initial" + +// User types "new text"... +console.log(input.getAttribute('value')) // Still "initial"! +console.log(input.value) // "new text" + +// Reset to attribute value +input.value = input.getAttribute('value') +``` + +Key differences: + +| Aspect | Attribute | Property | +|--------|-----------|----------| +| Source | HTML markup | DOM object | +| Access | `get/setAttribute()` | Direct property access | +| Updates | Manual only | Automatically with user interaction | +| Type | Always string | Can be any type | + +```javascript +// Attribute is always a string +checkbox.getAttribute('checked') // "" or null + +// Property is a boolean +checkbox.checked // true or false + +// Attribute (string) +input.getAttribute('maxlength') // "10" + +// Property (number) +input.maxLength // 10 +``` + +### Data Attributes and the dataset API + +Custom data attributes start with `data-` and are accessible via the **[`dataset`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset)** property: + +```html +<div id="user" + data-user-id="123" + data-role="admin" + data-is-active="true"> + John Doe +</div> +``` + +```javascript +const user = document.querySelector('#user') + +// Read data attributes (camelCase!) +console.log(user.dataset.userId) // "123" +console.log(user.dataset.role) // "admin" +console.log(user.dataset.isActive) // "true" (string, not boolean!) + +// Write data attributes +user.dataset.lastLogin = '2024-01-15' +// Creates: data-last-login="2024-01-15" + +// Delete data attributes +delete user.dataset.role + +// Check if exists +if ('userId' in user.dataset) { + console.log('Has user ID') +} +``` + +<Tip> +**Naming Convention:** HTML uses `kebab-case` (`data-user-id`), JavaScript uses `camelCase` (`dataset.userId`). The conversion is automatic! +</Tip> + +### Common Attribute Shortcuts + +Many attributes have direct property shortcuts: + +```javascript +// These pairs are equivalent: +element.id // element.getAttribute('id') +element.className // element.getAttribute('class') +element.href // element.getAttribute('href') +element.src // element.getAttribute('src') +element.title // element.getAttribute('title') + +// For class manipulation, use classList (covered next) +``` + +--- + +## How to Style DOM Elements with JavaScript + +JavaScript can modify element styles in several ways using the **[`style`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/style)** property and **[`classList`](https://developer.mozilla.org/en-US/docs/Web/API/Element/classList)** API. + +### The style Property (Inline Styles) + +```javascript +const box = document.querySelector('.box') + +// Set individual styles (camelCase!) +box.style.backgroundColor = 'blue' +box.style.fontSize = '20px' +box.style.marginTop = '10px' + +// Read styles (only reads INLINE styles!) +console.log(box.style.backgroundColor) // "blue" +console.log(box.style.color) // "" (not inline, from stylesheet) + +// Set multiple styles at once +box.style.cssText = 'background: red; font-size: 16px; padding: 10px;' + +// Remove an inline style +box.style.backgroundColor = '' // Removes the style +``` + +<Warning> +`element.style` only reads/writes **inline** styles! To get computed styles (from stylesheets), use `getComputedStyle()`. +</Warning> + +### getComputedStyle() - Read Actual Styles + +Use **[`getComputedStyle()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle)** to read the final computed styles: + +```javascript +const box = document.querySelector('.box') + +// Get all computed styles +const styles = getComputedStyle(box) + +console.log(styles.backgroundColor) // "rgb(0, 0, 255)" +console.log(styles.fontSize) // "16px" +console.log(styles.display) // "block" + +// Get pseudo-element styles +const beforeStyles = getComputedStyle(box, '::before') +console.log(beforeStyles.content) // '"Hello"' +``` + +### classList - Manipulate CSS Classes + +The **[`classList`](https://developer.mozilla.org/en-US/docs/Web/API/Element/classList)** API is the modern way to add/remove/toggle classes: + +```javascript +const button = document.querySelector('button') + +// Add classes +button.classList.add('active') +button.classList.add('btn', 'btn-primary') // Multiple at once + +// Remove classes +button.classList.remove('active') +button.classList.remove('btn', 'btn-primary') // Multiple at once + +// Toggle (add if missing, remove if present) +button.classList.toggle('active') + +// Toggle with condition +button.classList.toggle('active', isActive) // Add if isActive is true + +// Check if class exists +if (button.classList.contains('active')) { + console.log('Button is active') +} + +// Replace a class +button.classList.replace('btn-primary', 'btn-secondary') + +// Iterate over classes +button.classList.forEach(cls => console.log(cls)) + +// Get number of classes +console.log(button.classList.length) // 2 +``` + +### className vs classList + +```javascript +// className is a string (old way) +element.className = 'btn btn-primary' // Replaces ALL classes +element.className += ' active' // Appending is clunky + +// classList is a DOMTokenList (modern way) +element.classList.add('active') // Adds without affecting others +element.classList.remove('btn-primary') // Removes specifically +``` + +--- + +## How Browsers Render the DOM to Pixels + +Understanding how browsers render pages helps you write performant code. This is where [JavaScript Engines](/concepts/javascript-engines) and the browser's rendering engine work together. + +### From HTML to Pixels + +When you load a webpage, the browser goes through these steps: + +<Steps> + <Step title="1. Parse HTML → Build DOM"> + Browser reads HTML bytes and constructs the Document Object Model tree. + </Step> + <Step title="2. Parse CSS → Build CSSOM"> + CSS is parsed into the CSS Object Model with styling rules. + </Step> + <Step title="3. Combine → Render Tree"> + DOM + CSSOM merge into the Render Tree (only visible elements). + </Step> + <Step title="4. Layout (Reflow)"> + Calculate exact position and size of every element. + </Step> + <Step title="5. Paint"> + Fill in pixels: colors, borders, shadows, text. + </Step> + <Step title="6. Composite"> + Combine layers into the final image using the GPU. + </Step> +</Steps> + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ THE CRITICAL RENDERING PATH │ +│ │ +│ 1. PARSE HTML 2. PARSE CSS 3. BUILD RENDER TREE │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │ +│ │ HTML bytes │ │ CSS bytes │ │ DOM + CSSOM │ │ +│ │ ↓ │ │ ↓ │ │ ↘ ↙ │ │ +│ │ Characters │ │ Characters │ │ RENDER TREE │ │ +│ │ ↓ │ │ ↓ │ │ (visible elements │ │ +│ │ Tokens │ │ Tokens │ │ + their styles) │ │ +│ │ ↓ │ │ ↓ │ └──────────────────────┘ │ +│ │ Nodes │ │ Rules │ │ │ +│ │ ↓ │ │ ↓ │ ▼ │ +│ │ DOM │ │ CSSOM │ 4. LAYOUT (Reflow) │ +│ └──────────────┘ └──────────────┘ ┌──────────────────────┐ │ +│ │ Calculate exact │ │ +│ │ position & size of │ │ +│ │ every element │ │ +│ └──────────┬───────────┘ │ +│ │ │ +│ ▼ │ +│ 5. PAINT │ +│ ┌──────────────────────┐ │ +│ │ Fill in pixels: │ │ +│ │ colors, borders, │ │ +│ │ shadows, text │ │ +│ └──────────┬───────────┘ │ +│ │ │ +│ ▼ │ +│ 6. COMPOSITE │ +│ ┌──────────────────────┐ │ +│ │ Combine layers into │ │ +│ │ final image (GPU) │ │ +│ └──────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────────┐ │ +│ │ PIXELS! │ │ +│ └──────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +### What's NOT in the Render Tree + +The Render Tree only contains visible elements: + +```html +<!-- NOT in Render Tree --> +<head>...</head> <!-- head is never rendered --> +<script>...</script> <!-- script tags aren't visible --> +<link rel="stylesheet"> <!-- link tags aren't visible --> +<meta> <!-- meta tags aren't visible --> +<div style="display: none">Hi</div> <!-- display:none excluded --> + +<!-- IN the Render Tree (even if not seen) --> +<div style="visibility: hidden">Hi</div> <!-- Takes up space --> +<div style="opacity: 0">Hi</div> <!-- Takes up space --> +``` + +### Layout (Reflow) - The Expensive Step + +Layout calculates the **geometry** of every element: position, size, margins, etc. + +**Reflow is triggered when:** +- Adding/removing elements +- Changing element dimensions (width, height, padding, margin) +- Changing font size +- Resizing the window +- Reading certain properties (more on this below!) + +### Paint - Drawing Pixels + +After layout, the browser paints the pixels: text, colors, images, borders, shadows. + +**Repaint (without reflow) happens when:** +- Changing colors +- Changing background-image +- Changing visibility +- Changing box-shadow (sometimes) + +### Composite - Layering + +Modern browsers separate content into layers and use the GPU to composite them. This is why some animations are smooth: + +```css +/* These properties can animate without reflow/repaint */ +transform: translateX(100px); /* GPU accelerated! */ +opacity: 0.5; /* GPU accelerated! */ + +/* These properties cause reflow */ +left: 100px; /* Avoid for animations! */ +width: 200px; /* Avoid for animations! */ +``` + +--- + +## How to Optimize DOM Performance + +DOM operations can be slow. Here's how to keep your pages fast. + +### Cache DOM References + +```javascript +// Bad: Queries the DOM every iteration +for (let i = 0; i < 1000; i++) { + document.querySelector('.result').textContent += i +} + +// Good: Query once, reuse +const result = document.querySelector('.result') +for (let i = 0; i < 1000; i++) { + result.textContent += i +} + +// Even better: Build string, set once +const result = document.querySelector('.result') +let text = '' +for (let i = 0; i < 1000; i++) { + text += i +} +result.textContent = text +``` + +### Batch DOM Updates + +```javascript +// Avoid: Multiple style changes (may trigger multiple reflows) +element.style.width = '100px' +element.style.height = '200px' +element.style.margin = '10px' + +// Better: Single style assignment with cssText +element.style.cssText = 'width: 100px; height: 200px; margin: 10px;' + +// Best: Use a CSS class (cleanest and most maintainable) +element.classList.add('my-styles') + +// Good: DocumentFragment for multiple elements +const fragment = document.createDocumentFragment() +items.forEach(item => { + const li = document.createElement('li') + li.textContent = item + fragment.appendChild(li) +}) +ul.appendChild(fragment) // Single DOM update +``` + +<Tip> +**Why batch?** While modern browsers often optimize consecutive style changes into a single reflow, this optimization breaks if you read a layout property (like `offsetWidth`) between writes. Batching explicitly avoids this risk and makes your intent clear. +</Tip> + +### Avoid Layout Thrashing + +**Layout thrashing** occurs when you alternate between reading and writing DOM properties: + +```javascript +// TERRIBLE: Forces layout on EVERY iteration +boxes.forEach(box => { + const width = box.offsetWidth // Read (forces layout) + box.style.width = (width + 10) + 'px' // Write (invalidates layout) +}) + +// GOOD: Batch reads, then batch writes +const widths = boxes.map(box => box.offsetWidth) // Read all +boxes.forEach((box, i) => { + box.style.width = (widths[i] + 10) + 'px' // Write all +}) +``` + +**Properties that trigger layout when read:** + +| Property | What It Returns | +|----------|-----------------| +| `offsetWidth` / `offsetHeight` | Element's layout width/height including borders | +| `offsetTop` / `offsetLeft` | Position relative to offset parent | +| `clientWidth` / `clientHeight` | Inner dimensions (padding but no border) | +| `scrollWidth` / `scrollHeight` | Full scrollable dimensions | +| `scrollTop` / `scrollLeft` | Current scroll position | +| `getBoundingClientRect()` | Position and size relative to viewport | +| `getComputedStyle()` | All computed CSS values | + +```javascript +// Any of these reads forces a layout calculation +const width = element.offsetWidth // Layout triggered! +const rect = element.getBoundingClientRect() // Layout triggered! +const styles = getComputedStyle(element) // Layout triggered! +``` + +### Use requestAnimationFrame for Visual Changes + +Use **[`requestAnimationFrame()`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame)** to batch visual changes with the browser's render cycle: + +```javascript +// Bad: DOM changes at unpredictable times +window.addEventListener('scroll', () => { + element.style.transform = `translateY(${window.scrollY}px)` +}) + +// Good: Batch visual changes with next frame +let ticking = false +window.addEventListener('scroll', () => { + if (!ticking) { + requestAnimationFrame(() => { + element.style.transform = `translateY(${window.scrollY}px)` + ticking = false + }) + ticking = true + } +}) +``` + +--- + +## The #1 DOM Mistake: Using innerHTML with User Input + +The most dangerous DOM mistake is using **[`innerHTML`](https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML)** with untrusted content. This opens your application to **Cross-Site Scripting (XSS)** attacks. + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ innerHTML: THE SECURITY TRAP │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ❌ DANGEROUS ✓ SAFE │ +│ ───────────── ────── │ +│ │ +│ User Input: User Input: │ +│ "<img src=x onerror=alert('XSS')>" "<img src=x onerror=...>" │ +│ │ │ │ +│ ▼ ▼ │ +│ element.innerHTML = userInput element.textContent = input │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ BROWSER PARSES │ │ DISPLAYED AS │ │ +│ │ AS REAL HTML! │ │ PLAIN TEXT │ │ +│ │ │ │ │ │ +│ │ 🚨 Script runs! │ │ "<img src=..." │ │ +│ │ Cookies stolen! │ │ (harmless) │ │ +│ └─────────────────┘ └─────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +```javascript +// ❌ DANGEROUS - Never do this with user input! +const username = getUserInput() // User enters: <img src=x onerror="stealCookies()"> +div.innerHTML = `Welcome, ${username}!` +// The malicious script EXECUTES! + +// ✓ SAFE - textContent escapes HTML +const username = getUserInput() +div.textContent = `Welcome, ${username}!` +// Displays: Welcome, <img src=x onerror="stealCookies()">! +// The HTML is shown as text, not executed + +// ✓ SAFE - Create elements programmatically +const username = getUserInput() +const welcomeText = document.createTextNode(`Welcome, ${username}!`) +div.appendChild(welcomeText) +``` + +<Warning> +**The Trap:** `innerHTML` looks convenient, but it parses strings as real HTML. If that string contains user input, attackers can inject `<script>` tags, malicious event handlers, or other dangerous code. **Always use `textContent` for user-provided content.** +</Warning> + +### Other Common Mistakes + +<AccordionGroup> + <Accordion title="Forgetting that querySelector returns null"> + ```javascript + // ❌ WRONG - Crashes if element doesn't exist + document.querySelector('.maybe-missing').classList.add('active') + // TypeError: Cannot read property 'classList' of null + + // ✓ CORRECT - Check first or use optional chaining + const element = document.querySelector('.maybe-missing') + if (element) { + element.classList.add('active') + } + + // Or use optional chaining (modern) + document.querySelector('.maybe-missing')?.classList.add('active') + ``` + </Accordion> + + <Accordion title="Using childNodes instead of children"> + ```javascript + // ❌ CONFUSING - Includes whitespace text nodes! + const ul = document.querySelector('ul') + console.log(ul.childNodes.length) // 7 (includes text nodes!) + + // ✓ CLEAR - Only element children + console.log(ul.children.length) // 3 (just the <li> elements) + ``` + </Accordion> + + <Accordion title="Layout thrashing in loops"> + ```javascript + // ❌ SLOW - Forces layout on every iteration + boxes.forEach(box => { + const width = box.offsetWidth // READ - forces layout + box.style.width = width + 10 + 'px' // WRITE - invalidates layout + }) + + // ✓ FAST - Batch reads, then batch writes + const widths = boxes.map(box => box.offsetWidth) // All reads + boxes.forEach((box, i) => { + box.style.width = widths[i] + 10 + 'px' // All writes + }) + ``` + </Accordion> +</AccordionGroup> + +--- + +## Event Propagation: Bubbling and Capturing + +When an event occurs on a DOM element, it doesn't just trigger on that element. It travels through the DOM tree in a process called **event propagation**. Understanding this helps with event handling. + +### The Three Phases + +Every DOM event goes through three phases: + +``` +1. CAPTURING PHASE ↓ (from window → target's parent) +2. TARGET PHASE ● (at the target element) +3. BUBBLING PHASE ↑ (from target's parent → window) +``` + +```javascript +// Most events bubble UP by default +document.querySelector('.child').addEventListener('click', (e) => { + console.log('Child clicked') +}) + +document.querySelector('.parent').addEventListener('click', (e) => { + console.log('Parent also receives the click!') // This fires too! +}) +``` + +### Capturing vs Bubbling + +By default, event listeners fire during the **bubbling phase** (bottom-up). You can listen during the **capturing phase** (top-down) with the third parameter: + +```javascript +// Bubbling (default) — fires on the way UP +element.addEventListener('click', handler) +element.addEventListener('click', handler, false) + +// Capturing — fires on the way DOWN +element.addEventListener('click', handler, true) +element.addEventListener('click', handler, { capture: true }) +``` + +```javascript +// Practical example: see the order +document.querySelector('.parent').addEventListener('click', () => { + console.log('1. Parent - capturing') +}, true) + +document.querySelector('.child').addEventListener('click', () => { + console.log('2. Child - target') +}) + +document.querySelector('.parent').addEventListener('click', () => { + console.log('3. Parent - bubbling') +}) + +// Click on child outputs: 1, 2, 3 +``` + +### Stopping Propagation + +You can stop an event from traveling further: + +```javascript +element.addEventListener('click', (e) => { + e.stopPropagation() // Stop bubbling/capturing + // Parent handlers won't fire +}) + +element.addEventListener('click', (e) => { + e.stopImmediatePropagation() // Stop ALL handlers, even on same element +}) +``` + +<Warning> +**Use `stopPropagation()` sparingly!** It breaks event delegation and can make debugging difficult. Usually there's a better solution. +</Warning> + +### Preventing Default Behavior + +Don't confuse propagation with default behavior: + +```javascript +// Prevent the browser's default action (e.g., following a link) +link.addEventListener('click', (e) => { + e.preventDefault() // Don't navigate + // Event still bubbles unless you also call stopPropagation() +}) + +// Common use cases: +// - Prevent form submission: form.addEventListener('submit', e => e.preventDefault()) +// - Prevent link navigation: link.addEventListener('click', e => e.preventDefault()) +// - Prevent context menu: element.addEventListener('contextmenu', e => e.preventDefault()) +``` + +### The `event.target` vs `event.currentTarget` + +This distinction matters for event delegation: + +```javascript +document.querySelector('.parent').addEventListener('click', (e) => { + console.log(e.target) // The element that was actually clicked + console.log(e.currentTarget) // The element with the listener (.parent) + console.log(this) // Same as currentTarget (in regular functions) +}) +``` + +```javascript +// If you click on a <span> inside .parent: +// e.target = <span> (what you clicked) +// e.currentTarget = .parent (what has the listener) +``` + +### Events That Don't Bubble + +Most events bubble, but some don't: + +| Event | Bubbles? | Notes | +|-------|----------|-------| +| `click`, `mousedown`, `keydown` | Yes | Most user events bubble | +| `focus`, `blur` | No | Use `focusin`/`focusout` for bubbling versions | +| `mouseenter`, `mouseleave` | No | Use `mouseover`/`mouseout` for bubbling versions | +| `load`, `unload`, `scroll` | No | Window/document events | + +```javascript +// focus doesn't bubble, but focusin does +form.addEventListener('focusin', (e) => { + console.log('Something in the form was focused:', e.target) +}) +``` + +--- + +## Common DOM Patterns + +### Event Delegation + +Instead of adding listeners to many elements, add one to a parent. This pattern relies on **event bubbling**. When you click a child element, the event bubbles up to the parent where your listener catches it: + +```javascript +// Bad: Many listeners +document.querySelectorAll('.btn').forEach(btn => { + btn.addEventListener('click', handleClick) +}) + +// Good: One listener with delegation +document.querySelector('.button-container').addEventListener('click', (e) => { + const btn = e.target.closest('.btn') + if (btn) { + handleClick(e) + } +}) +``` + +Benefits: +- Works for dynamically added elements +- Less memory usage +- Easier cleanup (uses [closures](/concepts/scope-and-closures) to maintain handler references) + +### Checking if Element Exists + +```javascript +// Using querySelector (returns null if not found) +const element = document.querySelector('.maybe-exists') +if (element) { + element.textContent = 'Found!' +} + +// Optional chaining (modern) +document.querySelector('.maybe-exists')?.classList.add('active') + +// With getElementById +const el = document.getElementById('myId') +if (el !== null) { + // Element exists +} +``` + +### Waiting for DOM Ready + +Listen for the **[`DOMContentLoaded`](https://developer.mozilla.org/en-US/docs/Web/API/Document/DOMContentLoaded_event)** event to know when the DOM is ready: + +```javascript +// Modern: DOMContentLoaded (DOM ready, images may still be loading) +document.addEventListener('DOMContentLoaded', () => { + console.log('DOM is ready!') + // Safe to query elements +}) + +// Full page load (including images, stylesheets) +window.addEventListener('load', () => { + console.log('Everything loaded!') +}) + +// If script is at end of body, DOM is already ready +// <script src="app.js"></script> <!-- Just before </body> --> + +// Modern: defer attribute (script loads in parallel, runs after DOM ready) +// <script src="app.js" defer></script> +``` + +<Tip> +**Best practice:** Put your `<script>` tags just before `</body>` or use the `defer` attribute. Then you don't need to wait for DOMContentLoaded. +</Tip> + +--- + +## Common Misconceptions + +<AccordionGroup> + <Accordion title="Misconception 1: 'The DOM is the same as my HTML source code'"> + **Wrong!** The DOM is NOT your HTML file. The browser: + + 1. **Fixes errors** — Missing `<head>`, `<body>`, unclosed tags are auto-corrected + 2. **Normalizes structure** — Text outside elements gets wrapped properly + 3. **Reflects JavaScript changes** — DOM updates don't change your HTML file + + ```html + <!-- Your HTML file --> + <html>Hello World + + <!-- What the DOM looks like --> + <html> + <head></head> + <body>Hello World</body> + </html> + ``` + + View Source shows your file. DevTools Elements shows the DOM. + </Accordion> + + <Accordion title="Misconception 2: 'querySelector is slow, use getElementById'"> + **Mostly wrong!** Yes, `getElementById` is technically faster (O(1) hashtable lookup), but: + + - The difference is **microseconds** — imperceptible to users + - `querySelector` is more **flexible** and **readable** + - You'd need to call it **thousands of times in a loop** to notice + + ```javascript + // Both are fine for normal use + document.getElementById('myId') + document.querySelector('#myId') + + // Only optimize if you're selecting in a tight loop + // with performance issues (rare!) + ``` + + **Rule:** Write readable code first. Optimize only when you have a measured problem. + </Accordion> + + <Accordion title="Misconception 3: 'display: none removes the element from the DOM'"> + **Wrong!** `display: none` hides the element visually, but it's still in the DOM: + + ```javascript + element.style.display = 'none' + + // Element is STILL in the DOM! + console.log(document.getElementById('hidden')) // Element exists + console.log(element.parentNode) // Still has parent + + // To actually remove from DOM: + element.remove() + // or + element.parentNode.removeChild(element) + ``` + + - `display: none` → Hidden but in DOM, not in Render Tree + - `visibility: hidden` → Hidden but takes up space, in Render Tree + - `remove()` → Actually removed from DOM + </Accordion> + + <Accordion title="Misconception 4: 'Live collections automatically update my code'"> + **Misleading!** Live collections (`getElementsByClassName`, `getElementsByTagName`) update automatically, but this can cause bugs: + + ```javascript + const items = document.getElementsByClassName('item') + + // DANGER: Removing items changes the collection while looping! + for (let i = 0; i < items.length; i++) { + items[i].remove() // Collection shrinks, indices shift! + } + // Some items are skipped! + + // SAFE: Use static NodeList or convert to array + const items = document.querySelectorAll('.item') // Static + items.forEach(item => item.remove()) // Works correctly + ``` + + **Tip:** Prefer `querySelectorAll` (static) unless you specifically need live updates. + </Accordion> +</AccordionGroup> + +--- + +## Classic Interview Questions + +### Question 1: What's the difference between `document.querySelector` and `document.getElementById`? + +<Accordion title="Answer"> +| Feature | `getElementById` | `querySelector` | +|---------|-----------------|-----------------| +| Selector type | ID only | Any CSS selector | +| Returns | Element or `null` | Element or `null` | +| Speed | Faster (hashtable) | Slightly slower (parses CSS) | +| Flexibility | Low | High | + +```javascript +// getElementById — only IDs +document.getElementById('myId') + +// querySelector — any CSS selector +document.querySelector('#myId') // Same as above +document.querySelector('.card:first-child') // Not possible with getElementById +document.querySelector('[data-id="123"]') // Attribute selector +``` + +**Best answer:** "getElementById is marginally faster but querySelector is more flexible. In practice, the performance difference is negligible for most applications. I prefer querySelector for consistency and flexibility." +</Accordion> + +### Question 2: Explain event delegation and why it's useful + +<Accordion title="Answer"> +**Event delegation** is attaching a single event listener to a parent element instead of multiple listeners to child elements. It works because events "bubble up" the DOM tree. + +```javascript +// ❌ Without delegation — 100 listeners for 100 items +document.querySelectorAll('.item').forEach(item => { + item.addEventListener('click', handleClick) +}) + +// ✓ With delegation — 1 listener handles all items +document.querySelector('.container').addEventListener('click', (e) => { + const item = e.target.closest('.item') + if (item) handleClick(e) +}) +``` + +**Benefits:** +1. **Memory efficient** — One listener vs. many +2. **Works for dynamic elements** — New items automatically handled +3. **Easier cleanup** — Remove one listener to clean up + +**Best answer:** Include a code example and mention `closest()` for finding the target element. +</Accordion> + +### Question 3: What causes layout thrashing and how do you avoid it? + +<Accordion title="Answer"> +**Layout thrashing** occurs when you repeatedly alternate between reading and writing DOM layout properties, forcing the browser to recalculate layout multiple times. + +```javascript +// ❌ Thrashing — forces layout on EVERY iteration +boxes.forEach(box => { + const width = box.offsetWidth // READ → triggers layout + box.style.width = width + 10 + 'px' // WRITE → invalidates layout +}) + +// ✓ Batched — one layout calculation +const widths = boxes.map(box => box.offsetWidth) // All reads +boxes.forEach((box, i) => { + box.style.width = widths[i] + 10 + 'px' // All writes +}) +``` + +**Properties that trigger layout:** `offsetWidth/Height`, `clientWidth/Height`, `getBoundingClientRect()`, `getComputedStyle()` + +**Best answer:** Explain the read-write-read-write pattern and show the batched solution. +</Accordion> + +### Question 4: What's the difference between `innerHTML`, `textContent`, and `innerText`? + +<Accordion title="Answer"> +| Property | Parses HTML? | Includes hidden text? | Performance | Security | +|----------|-------------|----------------------|-------------|----------| +| `innerHTML` | Yes | Yes | Slower | XSS risk | +| `textContent` | No | Yes | Fast | Safe | +| `innerText` | No | No (respects CSS) | Slowest | Safe | + +```javascript +// <div id="el"><span style="display:none">Hidden</span> Visible</div> + +el.innerHTML // "<span style="display:none">Hidden</span> Visible" +el.textContent // "Hidden Visible" +el.innerText // " Visible" (hidden text excluded) +``` + +**Security warning:** Never use `innerHTML` with user input. It can execute malicious scripts (XSS attacks). Use `textContent` instead. + +**Best answer:** Mention the XSS security risk with innerHTML. This shows you understand real-world implications. +</Accordion> + +### Question 5: How do you efficiently add 1000 elements to the DOM? + +<Accordion title="Answer"> +Use a **DocumentFragment** to batch insertions: + +```javascript +// ❌ Slow — 1000 DOM updates, 1000 potential reflows +for (let i = 0; i < 1000; i++) { + const li = document.createElement('li') + li.textContent = `Item ${i}` + ul.appendChild(li) // Triggers update each time +} + +// ✓ Fast — 1 DOM update +const fragment = document.createDocumentFragment() +for (let i = 0; i < 1000; i++) { + const li = document.createElement('li') + li.textContent = `Item ${i}` + fragment.appendChild(li) // No DOM update (fragment is detached) +} +ul.appendChild(fragment) // Single update +``` + +**Alternative:** Build an HTML string and use `innerHTML` once (but only with trusted content, never user input). + +**Best answer:** Show the fragment approach and explain WHY it's faster (detached container, single reflow). +</Accordion> + +### Question 6: What's the difference between attributes and properties? + +<Accordion title="Answer"> +**Attributes** are defined in HTML. **Properties** are the live state on DOM objects. + +```html +<input type="text" value="initial"> +``` + +```javascript +const input = document.querySelector('input') + +// Attribute — original HTML value +input.getAttribute('value') // "initial" (never changes) + +// Property — current live value +input.value // "initial" initially, then whatever user types + +// User types "hello"... +input.getAttribute('value') // Still "initial" +input.value // "hello" +``` + +| Aspect | Attribute | Property | +|--------|-----------|----------| +| Source | HTML markup | DOM object | +| Type | Always string | Can be any type | +| Updates | Manual only | Automatically with interaction | + +**Best answer:** Use the `<input value="">` example. It's the clearest demonstration of the difference. +</Accordion> + +--- + +## Key Takeaways + +<Info> +**The key things to remember:** + +1. **The DOM is a tree** — Elements are nodes with parent, child, and sibling relationships + +2. **DOM ≠ HTML source** — The browser fixes errors and JavaScript modifies it + +3. **Use querySelector** — More flexible than getElementById, accepts any CSS selector + +4. **Element vs Node properties** — Use `children`, `firstElementChild`, etc. to skip text nodes + +5. **closest() is your friend** — Perfect for event delegation and finding ancestor elements + +6. **innerHTML is dangerous** — Never use with user input; use textContent instead + +7. **Attributes vs Properties** — Attributes are HTML source, properties are live DOM state + +8. **classList over className** — Use add/remove/toggle for cleaner class manipulation + +9. **Batch DOM operations** — Use DocumentFragment or build strings to minimize reflows + +10. **Avoid layout thrashing** — Don't alternate reading and writing layout properties +</Info> + +--- + +## Test Your Knowledge + +<AccordionGroup> + <Accordion title="Question 1: What's the difference between childNodes and children?"> + **Answer:** + + - `childNodes` returns ALL child nodes, including **text nodes** (whitespace!) and **comment nodes** + - `children` returns only **element nodes** + + ```javascript + // <ul> + // <li>One</li> + // <li>Two</li> + // </ul> + + ul.childNodes.length // 5 (text, li, text, li, text) + ul.children.length // 2 (li, li) + ``` + + **Rule:** Use `children` unless you specifically need text/comment nodes. + </Accordion> + + <Accordion title="Question 2: Why is innerHTML dangerous with user input?"> + **Answer:** `innerHTML` parses strings as HTML, enabling **Cross-Site Scripting (XSS)** attacks: + + ```javascript + // User input: <img src=x onerror="stealCookies()"> + div.innerHTML = userInput // Executes malicious code! + + // Safe: textContent escapes HTML + div.textContent = userInput // Displays as plain text + ``` + + Always sanitize HTML or use `textContent` for user-provided content. + </Accordion> + + <Accordion title="Question 3: What's the difference between getAttribute('value') and .value on an input?"> + **Answer:** + + - `getAttribute('value')` returns the **original HTML attribute** (initial value) + - `.value` property returns the **current value** (what user typed) + + ```javascript + // <input value="initial"> + // User types "hello" + + input.getAttribute('value') // "initial" + input.value // "hello" + ``` + + Attributes are the HTML source. Properties are the live DOM state. + </Accordion> + + <Accordion title="Question 4: What does closest() do and why is it useful?"> + **Answer:** `closest()` finds the nearest **ancestor** (including the element itself) that matches a selector: + + ```javascript + // <div class="card"> + // <button class="btn">Click</button> + // </div> + + btn.closest('.card') // Returns the parent div + btn.closest('button') // Returns btn itself (it matches!) + btn.closest('.modal') // null (no matching ancestor) + ``` + + **Super useful for event delegation:** + + ```javascript + document.addEventListener('click', (e) => { + const card = e.target.closest('.card') + if (card) { + // Handle click inside any card + } + }) + ``` + </Accordion> + + <Accordion title="Question 5: What causes layout thrashing and how do you avoid it?"> + **Answer:** Layout thrashing happens when you **alternate reading and writing** layout-triggering properties: + + ```javascript + // BAD: Read-write-read-write pattern + boxes.forEach(box => { + const width = box.offsetWidth // READ → forces layout + box.style.width = width + 10 + 'px' // WRITE → invalidates layout + }) + // Each iteration forces a new layout calculation! + + // GOOD: Batch reads, then batch writes + const widths = boxes.map(b => b.offsetWidth) // All reads + boxes.forEach((box, i) => { + box.style.width = widths[i] + 10 + 'px' // All writes + }) + // Only one layout calculation! + ``` + </Accordion> + + <Accordion title="Question 6: What's in the Render Tree vs the DOM?"> + **Answer:** The DOM contains **all nodes** from the HTML (plus JS modifications). The Render Tree contains only **visible elements** with their computed styles. + + **In DOM but NOT in Render Tree:** + - `<head>` and its contents + - `<script>`, `<link>`, `<meta>` tags + - Elements with `display: none` + + **In Render Tree:** + - Visible elements + - Elements with `visibility: hidden` (still take space) + - Elements with `opacity: 0` (still take space) + + Pseudo-elements (`::before`, `::after`) are in the Render Tree but NOT in the DOM. + </Accordion> + + <Accordion title="Question 7: getElementsByClassName vs querySelectorAll - what's different?"> + **Answer:** + + | Aspect | `getElementsByClassName` | `querySelectorAll` | + |--------|--------------------------|-------------------| + | Returns | HTMLCollection | NodeList | + | **Live** | **Yes** (updates automatically) | **No** (static snapshot) | + | Selector | Class name only | Any CSS selector | + | Speed | Slightly faster | Slightly slower | + + ```javascript + const live = document.getElementsByClassName('item') + const staticList = document.querySelectorAll('.item') + + // Add new element with class="item" + document.body.appendChild(newItem) + + live.length // Increased (live collection) + staticList.length // Same (static snapshot) + ``` + </Accordion> + + <Accordion title="Question 8: How do you safely add many elements to the DOM?"> + **Answer:** Use a **DocumentFragment** to batch insertions: + + ```javascript + const fragment = document.createDocumentFragment() + + for (let i = 0; i < 1000; i++) { + const li = document.createElement('li') + li.textContent = `Item ${i}` + fragment.appendChild(li) // No reflow (fragment is detached) + } + + ul.appendChild(fragment) // Single reflow! + ``` + + A DocumentFragment is a virtual container. When appended, only its children are inserted. The fragment disappears. + + Alternative: Build HTML string and use `innerHTML` once (but sanitize if user input!). + </Accordion> +</AccordionGroup> + +--- + +## Related Concepts + +<CardGroup cols={2}> + <Card title="Event Loop" icon="arrows-spin" href="/concepts/event-loop"> + How JavaScript handles async operations and DOM events + </Card> + <Card title="JavaScript Engines" icon="gear" href="/concepts/javascript-engines"> + How V8 and other engines parse and execute your DOM code + </Card> + <Card title="Scope and Closures" icon="layer-group" href="/concepts/scope-and-closures"> + Understanding variable scope in event handlers and callbacks + </Card> + <Card title="Design Patterns" icon="puzzle-piece" href="/concepts/design-patterns"> + Patterns like Observer for reactive DOM updates + </Card> +</CardGroup> + +--- + +## Reference + +<CardGroup cols={2}> + <Card title="Document Object Model (DOM) — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model"> + The comprehensive MDN reference for all DOM interfaces, methods, and properties. + </Card> + <Card title="Document Interface — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/Document"> + The Document interface representing the web page loaded in the browser. + </Card> + <Card title="Element Interface — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/Element"> + The base class for all element objects in a Document. + </Card> + <Card title="Node Interface — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/Node"> + The abstract base class for DOM nodes including elements, text, and comments. + </Card> + <Card title="NodeList Interface — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/NodeList"> + Collections of nodes returned by querySelectorAll and other methods. + </Card> + <Card title="HTMLCollection Interface — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLCollection"> + Live collections of elements returned by getElementsByClassName and similar. + </Card> +</CardGroup> + +## Articles + +<CardGroup cols={2}> + <Card title="Eloquent JavaScript: The Document Object Model" icon="book" href="https://eloquentjavascript.net/14_dom.html"> + A free book chapter with runnable code examples you can edit right in the browser. Includes exercises at the end to test your understanding. + </Card> + <Card title="How To Understand and Modify the DOM in JavaScript" icon="newspaper" href="https://www.digitalocean.com/community/tutorials/introduction-to-the-dom"> + Tania Rascia walks through each concept with side-by-side HTML and JavaScript examples. Great for visual learners who want to see code and results together. + </Card> + <Card title="What's the Document Object Model, and why you should know how to use it" icon="newspaper" href="https://medium.freecodecamp.org/whats-the-document-object-model-and-why-you-should-know-how-to-use-it-1a2d0bc5429d"> + Builds a simple project while explaining DOM concepts. Good if you learn better by building something rather than reading theory. + </Card> + <Card title="What is the DOM?" icon="newspaper" href="https://css-tricks.com/dom/"> + Short read that clears up the "DOM vs HTML source" confusion with visual examples. Explains why DevTools shows something different from View Source. + </Card> + <Card title="Traversing the DOM with JavaScript" icon="newspaper" href="https://zellwk.com/blog/dom-traversals/"> + Zell explains the difference between Node and Element traversal methods with clear diagrams. Includes the "whitespace text node" gotcha that trips up beginners. + </Card> + <Card title="DOM Tree" icon="newspaper" href="https://javascript.info/dom-nodes"> + Interactive examples you can edit and run in the browser. Part of a larger DOM tutorial series if you want to keep going deeper. + </Card> + <Card title="How to traverse the DOM in JavaScript" icon="newspaper" href="https://medium.com/javascript-in-plain-english/how-to-traverse-the-dom-in-javascript-d6555c335b4e"> + Covers every traversal method with console output screenshots. Useful reference when you forget which property to use for siblings vs children. + </Card> + <Card title="Render Tree Construction" icon="newspaper" href="https://web.dev/articles/critical-rendering-path/render-tree-construction"> + Google's official explanation of the Critical Rendering Path. Essential reading if you want to understand why some DOM operations are slow. + </Card> + <Card title="What, exactly, is the DOM?" icon="newspaper" href="https://bitsofco.de/what-exactly-is-the-dom/"> + Compares DOM vs HTML source vs Render Tree side by side with diagrams. Clears up the confusion about what DevTools actually shows you. + </Card> + <Card title="JavaScript DOM Tutorial" icon="newspaper" href="https://www.javascripttutorial.net/javascript-dom/"> + A multi-part tutorial organized by topic, so you can jump to exactly what you need. Each page is self-contained with try-it-yourself examples. + </Card> + <Card title="Event Propagation — MDN" icon="newspaper" href="https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events"> + MDN's guide to event handling including bubbling, capturing, and delegation patterns. + </Card> + <Card title="Bubbling and Capturing" icon="newspaper" href="https://javascript.info/bubbling-and-capturing"> + Animated diagrams showing events traveling up and down the DOM tree. Makes the three-phase model (capture, target, bubble) easy to visualize. + </Card> +</CardGroup> + +## Videos + +<CardGroup cols={2}> + <Card title="JavaScript DOM Manipulation – Full Course for Beginners" icon="graduation-cap" href="https://www.youtube.com/watch?v=5fb2aPlgoys"> + A 2-hour freeCodeCamp course that builds multiple projects while teaching DOM concepts. Good if you want structured learning from zero to comfortable. + </Card> + <Card title="JavaScript DOM Tutorial" icon="video" href="https://www.youtube.com/watch?v=FIORjGvT0kk"> + A playlist of short, focused videos (5-10 min each). Pick the topic you need instead of watching everything in order. + </Card> + <Card title="JavaScript DOM Crash Course" icon="video" href="https://www.youtube.com/watch?v=0ik6X4DJKCc"> + Brad Traversy's 4-part series (this is part 1). Builds a task list project by the end, so you see DOM skills applied to something real. + </Card> + <Card title="JavaScript DOM Manipulation Methods" icon="video" href="https://www.youtube.com/watch?v=y17RuWkWdn8"> + Web Dev Simplified explains createElement, appendChild, and other manipulation methods. + </Card> + <Card title="JavaScript DOM Traversal Methods" icon="video" href="https://www.youtube.com/watch?v=v7rSSy8CaYE"> + Web Dev Simplified covers parent, child, and sibling traversal methods. + </Card> + <Card title="Event Propagation - JavaScript Event Bubbling and Propagation" icon="video" href="https://www.youtube.com/watch?v=JYc7gr9Ehl0"> + Steve Griffith explains event bubbling, capturing, and how to control event flow. + </Card> +</CardGroup> diff --git a/docs/concepts/equality-operators.mdx b/docs/concepts/equality-operators.mdx new file mode 100644 index 00000000..f33775b3 --- /dev/null +++ b/docs/concepts/equality-operators.mdx @@ -0,0 +1,1603 @@ +--- +title: "Equality Operators: == vs === Type Checking in JavaScript" +sidebarTitle: "Equality Operators: == vs === Type Checking" +description: "Learn JavaScript equality operators == vs ===, typeof quirks, and Object.is(). Understand type coercion, why NaN !== NaN, and why typeof null returns 'object'." +--- + +Why does `1 == "1"` return `true` but `1 === "1"` return `false`? Why does `typeof null` return `"object"`? And why is `NaN` the only value in JavaScript that isn't equal to itself? + +```javascript +// Same values, different results +console.log(1 == "1"); // true — loose equality converts types +console.log(1 === "1"); // false — strict equality checks type first + +// The famous quirks +console.log(typeof null); // "object" — a bug from 1995! +console.log(NaN === NaN); // false — NaN never equals anything +``` + +Understanding JavaScript's **[equality operators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Equality)** is crucial because comparison bugs are among the most common in JavaScript code. This guide will teach you exactly how `==`, **[`===`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Strict_equality)**, and **[`Object.is()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is)** work, and when to use each one. + +<Info> +**What you'll learn in this guide:** +- The difference between `==` (loose) and `===` (strict) equality +- How JavaScript converts values during loose equality comparisons +- The **[`typeof`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof)** operator and its famous quirks (including the `null` bug) +- When to use `Object.is()` for edge cases like `NaN` and `-0` +- Common comparison mistakes and how to avoid them +- A simple rule: when to use which operator +</Info> + +<Warning> +**Prerequisites:** This guide assumes you understand [Primitive Types](/concepts/primitive-types) and [Type Coercion](/concepts/type-coercion). Equality operators rely heavily on how JavaScript converts types. If those concepts are new to you, read those guides first! +</Warning> + +--- + +## The Three Equality Operators: Overview + +JavaScript provides three ways to compare values for equality. Here's the quick summary: + +| Operator | Name | Type Coercion | Best For | +|----------|------|---------------|----------| +| `==` | Loose (Abstract) Equality | Yes | Checking `null`/`undefined` only | +| `===` | Strict Equality | No | **Default choice for everything** | +| `Object.is()` | Same-Value Equality | No | Edge cases (`NaN`, `±0`) | + +```javascript +// The same comparison, three different results +const num = 1; +const str = "1"; + +console.log(num == str); // true (coerces string to number) +console.log(num === str); // false (different types) +console.log(Object.is(num, str)); // false (different types) +``` + +<Note> +**The simple rule:** Always use `===` for comparisons. The only exception: use `== null` to check if a value is empty (null or undefined). You'll rarely need `Object.is()`. It's for special cases we'll cover later. +</Note> + +--- + +## The Teacher Grading Papers: A Real-World Analogy + +Imagine a teacher grading a math test. The question asks: "What is 2 + 2?" + +One student writes: `4` +Another student writes: `"4"` (as text) +A third student writes: `4.0` + +How strict should the teacher be when grading? + +``` + RELAXED GRADING (==) STRICT GRADING (===) + "Is the answer correct?" "Is it exactly right?" + + ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ + │ 4 │ = │ "4" │ │ 4 │ ≠ │ "4" │ + │ (number) │ │ (string) │ │ (number) │ │ (string) │ + └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ + │ │ │ │ + └────────┬────────┘ └────────┬────────┘ + ▼ ▼ + "Close enough!" ✓ "Different types!" ✗ +``` + +JavaScript gives you both types of teachers: + +- **Loose equality (`==`)** — The relaxed teacher. Accepts `4` and `"4"` as the same answer because the *meaning* is similar. Converts values to match before comparing. +- **Strict equality (`===`)** — The strict teacher. Only accepts the *exact* answer in the *exact* format. The number `4` and the string `"4"` are different answers. +- **`typeof`** — Asks "What kind of answer is this?" Is it a number? A string? Something else? +- **`Object.is()`** — The most precise teacher. Even stricter than `===` — can spot tiny differences that others miss. + +<Tip> +**TL;DR:** Use `===` for almost everything. Use `== null` to check for both `null` and `undefined`. Use `Object.is()` only for `NaN` or `-0` edge cases. +</Tip> + +--- + +## Loose Equality (`==`): The Relaxed Comparison + +The **[`==` operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Equality)** tries to be helpful. Before comparing two values, it converts them to the same type. This automatic conversion is called **[type coercion](/concepts/type-coercion)**. + +For example, if you compare the number `5` with the string `"5"`, JavaScript thinks: "These look similar. Let me convert them and check." So `5 == "5"` returns `true`. + +### How It Works + +When you write `x == y`, JavaScript asks: + +1. Are `x` and `y` the same type? → Compare them directly +2. Are they different types? → Convert one or both to match, then compare + +This automatic conversion can be helpful, but it can also cause unexpected results. + +### The Abstract Equality Comparison Algorithm + +Here's the complete algorithm from the ECMAScript specification. When comparing `x == y`: + +<Steps> + <Step title="Same Type?"> + If `x` and `y` are the same type, perform strict equality comparison (`===`). + + ```javascript + 5 == 5 // Same type (number), compare directly → true + "hello" == "hello" // Same type (string), compare directly → true + ``` + </Step> + + <Step title="null and undefined"> + If `x` is **[`null`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/null)** and `y` is **[`undefined`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/undefined)** (or vice versa), return `true`. + + ```javascript + null == undefined // true (special case!) + undefined == null // true + ``` + </Step> + + <Step title="Number and String"> + If one is a Number and the other is a String, convert the String to a Number. + + ```javascript + 5 == "5" // "5" → 5, then 5 == 5 → true + 0 == "" // "" → 0, then 0 == 0 → true + 42 == "42" // "42" → 42, then 42 == 42 → true + ``` + </Step> + + <Step title="BigInt and String"> + If one is a **[BigInt](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt)** and the other is a String, convert the String to a BigInt. + + ```javascript + 10n == "10" // "10" → 10n, then 10n == 10n → true + ``` + </Step> + + <Step title="Boolean Conversion"> + If either value is a Boolean, convert it to a Number (`true` → `1`, `false` → `0`). + + ```javascript + true == 1 // true → 1, then 1 == 1 → true + false == 0 // false → 0, then 0 == 0 → true + true == "1" // true → 1, then 1 == "1" → 1 == 1 → true + ``` + </Step> + + <Step title="Object to Primitive"> + If one is an Object and the other is a String, Number, BigInt, or **[Symbol](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol)**, convert the Object to a primitive using **[`ToPrimitive`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toPrimitive)**. + + ```javascript + [1] == 1 // [1] → "1" → 1, then 1 == 1 → true + [""] == 0 // [""] → "" → 0, then 0 == 0 → true + ``` + </Step> + + <Step title="BigInt and Number"> + If one is a BigInt and the other is a Number, compare their mathematical values. + + ```javascript + 10n == 10 // Compare values: 10 == 10 → true + 10n == 10.5 // 10 !== 10.5 → false + ``` + </Step> + + <Step title="No Match"> + If none of the above rules apply, return `false`. + + ```javascript + null == 0 // false (null only equals undefined) + undefined == 0 // false + Symbol() == Symbol() // false (Symbols are always unique) + ``` + </Step> +</Steps> + +### Visual: The Coercion Decision Tree + +``` + x == y + │ + ┌────────────┴────────────┐ + ▼ ▼ + Same type? Different types? + │ │ + YES YES + │ │ + ▼ ▼ + Compare values ┌────────┴────────┐ + (like ===) │ │ + ▼ ▼ + null == undefined? Apply coercion + │ rules above + YES │ + │ ▼ + ▼ Convert types + true then compare + again +``` + +### The Complete Coercion Rules Table + +| Type of x | Type of y | Coercion Applied | +|-----------|-----------|------------------| +| Number | String | `ToNumber(y)` — String becomes Number | +| String | Number | `ToNumber(x)` — String becomes Number | +| BigInt | String | `ToBigInt(y)` — String becomes BigInt | +| String | BigInt | `ToBigInt(x)` — String becomes BigInt | +| Boolean | Any | `ToNumber(x)` — Boolean becomes Number (0 or 1) | +| Any | Boolean | `ToNumber(y)` — Boolean becomes Number (0 or 1) | +| Object | String/Number/BigInt/Symbol | `ToPrimitive(x)` — Object becomes primitive | +| String/Number/BigInt/Symbol | Object | `ToPrimitive(y)` — Object becomes primitive | +| BigInt | Number | Compare mathematical values directly | +| Number | BigInt | Compare mathematical values directly | +| null | undefined | `true` (special case) | +| undefined | null | `true` (special case) | +| null | Any (except undefined) | `false` | +| undefined | Any (except null) | `false` | + +### Surprising Results Gallery + +Here are some comparison results that surprise most developers. Understanding *why* these happen will help you avoid bugs in your code: + +<Tabs> + <Tab title="String & Number"> + ```javascript + // String converted to Number + 1 == "1" // true ("1" → 1) + 0 == "" // true ("" → 0) + 0 == "0" // true ("0" → 0) + 100 == "1e2" // true ("1e2" → 100) + + // But string-to-string is direct comparison + "" == "0" // false (both strings, different values) + + // NaN conversions (NaN is "Not a Number") + NaN == "NaN" // false (NaN ≠ anything, including itself) + 0 == "hello" // false ("hello" → NaN, 0 ≠ NaN) + ``` + </Tab> + + <Tab title="Boolean Coercion"> + ```javascript + // Booleans become 0 or 1 FIRST + true == 1 // true (true → 1) + false == 0 // true (false → 0) + true == "1" // true (true → 1, "1" → 1) + false == "" // true (false → 0, "" → 0) + + // This is why these are confusing: + true == "true" // false! (true → 1, "true" → NaN) + false == "false" // false! (false → 0, "false" → NaN) + + // And these seem wrong: + true == 2 // false (true → 1, 1 ≠ 2) + true == "2" // false (true → 1, "2" → 2, 1 ≠ 2) + ``` + + <Warning> + **Common trap:** `true == "true"` is `false`! The boolean `true` becomes `1`, and the string `"true"` becomes `NaN`. Since `1 ≠ NaN`, the result is `false`. + </Warning> + </Tab> + + <Tab title="null & undefined"> + ```javascript + // The special relationship + null == undefined // true (special rule!) + undefined == null // true + + // But they don't equal anything else + null == 0 // false + null == false // false + null == "" // false + undefined == 0 // false + undefined == false // false + undefined == "" // false + + // This is actually useful! + let value = null; + if (value == null) { + // Catches both null AND undefined + console.log("Value is nullish"); + } + ``` + + <Tip> + This is the ONE legitimate use case for `==`. Using `value == null` checks for both `null` and `undefined` in a single comparison. + </Tip> + </Tab> + + <Tab title="Arrays & Objects"> + ```javascript + // Arrays convert via ToPrimitive (usually toString) + [] == false // true ([] → "" → 0, false → 0) + [] == 0 // true ([] → "" → 0) + [] == "" // true ([] → "") + [1] == 1 // true ([1] → "1" → 1) + [1] == "1" // true ([1] → "1") + [1,2] == "1,2" // true ([1,2] → "1,2") + + // Empty array gotcha + [] == ![] // true! (see explanation below) + + // Objects with valueOf/toString + let obj = { valueOf: () => 42 }; + obj == 42 // true (obj.valueOf() → 42) + ``` + </Tab> +</Tabs> + +### Step-by-Step Trace: `[] == ![]` + +This is one of JavaScript's most surprising results. An empty array `[]` equals `![]`? Let's break down why this happens step by step: + +<Steps> + <Step title="Evaluate ![]"> + First, JavaScript evaluates `![]`. + - `[]` is truthy (all objects are truthy) + - `![]` therefore equals `false` + - Now we have: `[] == false` + </Step> + + <Step title="Boolean to Number"> + One side is a Boolean, so convert it to a Number. + - `false` → `0` + - Now we have: `[] == 0` + </Step> + + <Step title="Object to Primitive"> + One side is an Object, so convert it via ToPrimitive. + - `[]` → `""` (empty array's toString returns empty string) + - Now we have: `"" == 0` + </Step> + + <Step title="String to Number"> + One side is a String and one is a Number, so convert the String. + - `""` → `0` (empty string becomes 0) + - Now we have: `0 == 0` + </Step> + + <Step title="Final Comparison"> + Both sides are Numbers with the same value. + - `0 == 0` → `true` + </Step> +</Steps> + +```javascript +// The chain of conversions: +[] == ![] +[] == false // ![] → false +[] == 0 // false → 0 +"" == 0 // [] → "" +0 == 0 // "" → 0 +true // 0 equals 0! +``` + +<Warning> +This example shows why `==` can produce unexpected results. An empty array appears to equal its own negation! This isn't a bug. It's how JavaScript's conversion rules work. This is why most developers prefer `===`. +</Warning> + +### When `==` Might Be Useful + +Despite its quirks, there's one legitimate use case for loose equality: + +```javascript +// Checking for null OR undefined in one comparison +function greet(name) { + // Using == (the one acceptable use case!) + if (name == null) { + return "Hello, stranger!"; + } + return `Hello, ${name}!`; +} + +// Both null and undefined are caught +greet(null); // "Hello, stranger!" +greet(undefined); // "Hello, stranger!" +greet("Alice"); // "Hello, Alice!" +greet(""); // "Hello, !" (empty string is NOT null) +greet(0); // "Hello, 0!" (0 is NOT null) +``` + +This is equivalent to the more verbose: +```javascript +function greet(name) { + if (name === null || name === undefined) { + return "Hello, stranger!"; + } + return `Hello, ${name}!`; +} +``` + +<Tip> +Many style guides (including those from Airbnb and StandardJS) make an exception for `value == null` because it's a clean way to check for "nullish" values. However, you can also use the nullish coalescing operator (`??`) or optional chaining (`?.`) introduced in ES2020. +</Tip> + +--- + +## Strict Equality (`===`): The Reliable Choice + +The strict equality operator compares two values **without** any conversion. If the types are different, it immediately returns `false`. + +This is the operator you should use almost always. It's simple and predictable: the number `1` and the string `"1"` are different types, so `1 === "1"` returns `false`. No surprises. + +### How It Works + +When you write `x === y`, JavaScript asks: + +1. Are `x` and `y` the same type? No → return `false` +2. Same type? → Compare their values + +That's it. No conversions, no surprises (well, *almost*. There's one special case with **[`NaN`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NaN)**). + +### The Strict Equality Comparison Algorithm + +<Steps> + <Step title="Type Check"> + If `x` and `y` are different types, return `false` immediately. + + ```javascript + 1 === "1" // false (number vs string) + true === 1 // false (boolean vs number) + null === undefined // false (null vs undefined) + ``` + </Step> + + <Step title="Number Comparison"> + If both are Numbers: + - If either is `NaN`, return `false` + - If both are the same numeric value, return `true` + - `+0` and `-0` are considered equal + + ```javascript + 42 === 42 // true + NaN === NaN // false (!) + +0 === -0 // true + Infinity === Infinity // true + ``` + </Step> + + <Step title="String Comparison"> + If both are Strings, return `true` if they have the same characters in the same order. + + ```javascript + "hello" === "hello" // true + "hello" === "Hello" // false (case sensitive) + "hello" === "hello " // false (different length) + ``` + </Step> + + <Step title="Boolean Comparison"> + If both are Booleans, return `true` if they're both `true` or both `false`. + + ```javascript + true === true // true + false === false // true + true === false // false + ``` + </Step> + + <Step title="BigInt Comparison"> + If both are BigInts, return `true` if they have the same mathematical value. + + ```javascript + 10n === 10n // true + 10n === 20n // false + ``` + </Step> + + <Step title="Symbol Comparison"> + If both are Symbols, return `true` only if they are the exact same Symbol. + + ```javascript + const sym = Symbol("id"); + sym === sym // true + Symbol("id") === Symbol("id") // false (different symbols!) + ``` + </Step> + + <Step title="Object Comparison (Reference)"> + If both are Objects (including Arrays and Functions), return `true` only if they are the **same object** (same reference in memory). + + ```javascript + const obj = { a: 1 }; + obj === obj // true (same reference) + { a: 1 } === { a: 1 } // false (different objects!) + [] === [] // false (different arrays!) + ``` + </Step> + + <Step title="null and undefined"> + `null === null` returns `true`. `undefined === undefined` returns `true`. But `null === undefined` returns `false` (different types). + + ```javascript + null === null // true + undefined === undefined // true + null === undefined // false + ``` + </Step> +</Steps> + +### Visual: Strict Equality Flowchart + +``` + x === y + │ + ┌───────────────┴───────────────┐ + │ Same type? │ + └───────────────┬───────────────┘ + │ │ + NO YES + │ │ + ▼ ▼ + false Both NaN? + │ + ┌───────┴───────┐ + YES NO + │ │ + ▼ ▼ + false Same value? + (NaN never equals │ + anything!) ┌───────┴───────┐ + YES NO + │ │ + ▼ ▼ + true false +``` + +### The Predictable Results + +With `===`, what you see is what you get: + +```javascript +// All of these are false (different types) +1 === "1" // false +0 === "" // false +true === 1 // false +false === 0 // false +null === undefined // false +[] === "" // false + +// All of these are true (same type, same value) +1 === 1 // true +"hello" === "hello" // true +true === true // true +null === null // true +undefined === undefined // true +``` + +### Special Cases: Two Exceptions to Know + +Even `===` has two edge cases that might surprise you: + +<Tabs> + <Tab title="NaN !== NaN"> + ```javascript + // NaN is the only value that is not equal to itself + NaN === NaN // false! + + // NaN doesn't equal anything, not even itself! + // This is part of how numbers work in all programming languages + + // This is by design (IEEE 754 specification) + // NaN represents "Not a Number" - an undefined result + // Since it's not a specific number, it can't equal anything + + // How to check for NaN: + Number.isNaN(NaN) // true (recommended) + isNaN(NaN) // true (but has quirks — see below) + Object.is(NaN, NaN) // true (ES6) + + // The isNaN() quirk: + isNaN("hello") // true! (converts to NaN first) + Number.isNaN("hello") // false (no conversion) + ``` + + <Warning> + Always use **[`Number.isNaN()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isNaN)** instead of the global `isNaN()`. The global `isNaN()` function converts its argument to a Number first, which means `isNaN("hello")` returns `true`. That's rarely what you want. + </Warning> + </Tab> + + <Tab title="+0 === -0"> + ```javascript + // Positive zero and negative zero are considered equal + +0 === -0 // true + -0 === 0 // true + + // But they ARE different! Watch this: + 1 / +0 // Infinity + 1 / -0 // -Infinity + + // Two zeros, two different infinities. Math is wild. + + // How to distinguish them: + Object.is(+0, -0) // false (ES6) + 1 / +0 === 1 / -0 // false (Infinity vs -Infinity) + + // When does -0 appear? + 0 * -1 // -0 + Math.sign(-0) // -0 + JSON.parse("-0") // -0 + ``` + + You'll rarely need to tell `+0` and `-0` apart unless you're doing advanced math or physics calculations. + </Tab> +</Tabs> + +### Object Comparison: Reference vs Value + +This is one of the most important concepts to understand: + +```javascript +// Objects are compared by REFERENCE, not by value +const obj1 = { name: "Alice" }; +const obj2 = { name: "Alice" }; +const obj3 = obj1; + +obj1 === obj2 // false (different objects in memory) +obj1 === obj3 // true (same reference) + +// Same with arrays +const arr1 = [1, 2, 3]; +const arr2 = [1, 2, 3]; +const arr3 = arr1; + +arr1 === arr2 // false (different arrays) +arr1 === arr3 // true (same reference) + +// And functions +const fn1 = () => {}; +const fn2 = () => {}; +const fn3 = fn1; + +fn1 === fn2 // false (different functions) +fn1 === fn3 // true (same reference) +``` + +``` +MEMORY VISUALIZATION: + +obj1 ──────┐ + ├──► { name: "Alice" } (Object A) +obj3 ──────┘ + +obj2 ──────────► { name: "Alice" } (Object B) + +obj1 === obj3 → true (both point to Object A) +obj1 === obj2 → false (different objects, even with same content) +``` + +<Tip> +To compare objects by their content (deep equality), you need to: +- Use **[`JSON.stringify()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify)** for simple objects (has limitations) +- Write a recursive comparison function +- Use a library like Lodash's `_.isEqual()` +</Tip> + +--- + +## `Object.is()`: Same-Value Equality + +ES6 introduced `Object.is()` to fix the two edge cases where `===` gives unexpected results. It works exactly like `===`, but handles `NaN` and `-0` correctly. + +### Why It Exists + +```javascript +// The two cases where === is "wrong" +NaN === NaN // false (but NaN IS NaN!) ++0 === -0 // true (but they ARE different!) + +// Object.is() fixes both +Object.is(NaN, NaN) // true ✓ +Object.is(+0, -0) // false ✓ +``` + +### How It Differs from `===` + +`Object.is()` behaves exactly like `===` except for these two cases: + +| Expression | `===` | `Object.is()` | +|------------|-------|---------------| +| `NaN, NaN` | `false` | `true` | +| `+0, -0` | `true` | `false` | +| `-0, 0` | `true` | `false` | +| `1, 1` | `true` | `true` | +| `"a", "a"` | `true` | `true` | +| `null, null` | `true` | `true` | +| `{}, {}` | `false` | `false` | + +### Complete Comparison Table + +| Values | `==` | `===` | `Object.is()` | +|--------|------|-------|---------------| +| `1, "1"` | `true` | `false` | `false` | +| `0, false` | `true` | `false` | `false` | +| `null, undefined` | `true` | `false` | `false` | +| `NaN, NaN` | `false` | `false` | `true` | +| `+0, -0` | `true` | `true` | `false` | +| `[], []` | `false` | `false` | `false` | +| `{}, {}` | `false` | `false` | `false` | + +### When to Use `Object.is()` + +```javascript +// 1. Checking for NaN (alternative to Number.isNaN) +function isReallyNaN(value) { + return Object.is(value, NaN); +} + +// 2. Distinguishing +0 from -0 (rare, but needed in math/physics) +function isNegativeZero(value) { + return Object.is(value, -0); +} + +// 3. Implementing SameValue comparison (like in Map/Set) +// Maps use SameValueZero (like Object.is but +0 === -0) +const map = new Map(); +map.set(NaN, "value"); +map.get(NaN); // "value" (NaN works as a key!) + +// 4. Library code and polyfills +// When you need exact specification compliance +``` + +<Note> +For most everyday code, you won't need `Object.is()`. Use `===` as your default, and reach for `Object.is()` only when you specifically need to handle `NaN` or `±0` edge cases. +</Note> + +--- + +## The `typeof` Operator + +The **[`typeof` operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof)** tells you what type a value is. It returns a string like `"number"`, `"string"`, or `"boolean"`. It's very useful, but it has some famous quirks that surprise many developers. + +### How It Works + +```javascript +typeof operand +typeof(operand) // Both forms are valid +``` + +### Complete Results Table + +| Value | `typeof` Result | Notes | +|-------|-----------------|-------| +| `"hello"` | `"string"` | | +| `42` | `"number"` | Includes `Infinity`, `NaN` | +| `42n` | `"bigint"` | ES2020 | +| `true` / `false` | `"boolean"` | | +| `undefined` | `"undefined"` | | +| `Symbol()` | `"symbol"` | ES6 | +| `null` | `"object"` | **Famous bug!** | +| `{}` | `"object"` | | +| `[]` | `"object"` | Arrays are objects | +| `function(){}` | `"function"` | Special case | +| `class {}` | `"function"` | Classes are functions | +| `new Date()` | `"object"` | | +| `/regex/` | `"object"` | | + +### The Famous Quirks + +<AccordionGroup> + <Accordion title="typeof null === 'object' — A Famous Bug"> + ```javascript + typeof null // "object" — Wait, what?! + ``` + + **Why?** This is a bug from JavaScript's first version in 1995. In the original code, values were stored with a type tag. Objects had the tag `000`, and `null` was represented as `0x00`, which also matched the object tag. + + **Why wasn't it fixed?** Too much existing code depends on this behavior. Changing it now would break millions of websites. So we have to work around it. + + **Workaround:** + ```javascript + // Always check for null explicitly + function getType(value) { + if (value === null) return "null"; + return typeof value; + } + + // Or check for "real" objects + if (value !== null && typeof value === "object") { + // It's definitely an object (not null) + } + ``` + </Accordion> + + <Accordion title="Arrays Return 'object'"> + ```javascript + typeof [] // "object" + typeof [1, 2, 3] // "object" + typeof new Array() // "object" + ``` + + **Why?** Arrays ARE objects in JavaScript. They inherit from `Object.prototype` and have special behavior for numeric keys and the `length` property, but they're still objects. + + **How to check for arrays:** + ```javascript + Array.isArray([]) // true (recommended) + Array.isArray({}) // false + Array.isArray("hello") // false + + // Or using Object.prototype.toString + Object.prototype.toString.call([]) // "[object Array]" + ``` + + Use **[`Array.isArray()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray)**. It's the most reliable method. + </Accordion> + + <Accordion title="Functions Return 'function'"> + ```javascript + typeof function() {} // "function" + typeof (() => {}) // "function" + typeof class {} // "function" + typeof Math.sin // "function" + ``` + + **Why is this different?** Functions are technically objects too, but `typeof` treats them specially because checking for "callable" values is so common. This is actually convenient! + + ```javascript + // This makes checking for functions easy + if (typeof callback === "function") { + callback(); + } + ``` + </Accordion> + + <Accordion title="typeof on Undeclared Variables"> + ```javascript + // Referencing an undeclared variable throws an error + console.log(undeclaredVar); // ReferenceError! + + // But typeof on an undeclared variable returns "undefined" + typeof undeclaredVar // "undefined" (no error!) + ``` + + **Why?** This was a design decision to allow safe feature detection: + + ```javascript + // Safe way to check if a global exists + if (typeof jQuery !== "undefined") { + // jQuery is available + } + + // vs. this would throw if jQuery doesn't exist + if (jQuery !== undefined) { + // ReferenceError if jQuery not defined! + } + ``` + + <Note> + In modern JavaScript with modules and bundlers, this pattern is less necessary. But it's still useful for checking global variables and browser features. + </Note> + </Accordion> + + <Accordion title="NaN is a 'number' — Yes, Really"> + ```javascript + typeof NaN // "number" + ``` + + "Not a Number" has a typeof of `"number"`. This sounds strange, but `NaN` is actually a special value in the number system. It represents a calculation that doesn't have a valid result, like `0 / 0`. + + ```javascript + // These all produce NaN + 0 / 0 // NaN + parseInt("hello") // NaN + Math.sqrt(-1) // NaN + + // Check for NaN properly + Number.isNaN(NaN) // true + ``` + </Accordion> +</AccordionGroup> + +### Better Alternatives for Type Checking + +Since `typeof` has limitations, here are more reliable approaches: + +<Tabs> + <Tab title="Type-Specific Checks"> + ```javascript + // Arrays + Array.isArray(value) // true for arrays only + + // NaN + Number.isNaN(value) // true for NaN only (no coercion) + + // Finite numbers + Number.isFinite(value) // true for finite numbers + + // Integers + Number.isInteger(value) // true for integers + + // Safe integers + Number.isSafeInteger(value) // true for safe integers + ``` + + These methods from **[`Number`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** are more reliable than `typeof` for numeric checks. + </Tab> + + <Tab title="instanceof"> + The **[`instanceof` operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/instanceof)** checks if an object is an instance of a constructor: + + ```javascript + // Check if an object is an instance of a constructor + [] instanceof Array // true + {} instanceof Object // true + new Date() instanceof Date // true + /regex/ instanceof RegExp // true + + // Works with custom classes + class Person {} + const p = new Person(); + p instanceof Person // true + + // Caveat: doesn't work across iframes/realms + // The Array in iframe A is different from Array in iframe B + ``` + </Tab> + + <Tab title="Object.prototype.toString"> + The **[`Object.prototype.toString`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString)** method is the most reliable for getting precise type information: + + ```javascript + const getType = (value) => + Object.prototype.toString.call(value).slice(8, -1); + + getType(null) // "Null" + getType(undefined) // "Undefined" + getType([]) // "Array" + getType({}) // "Object" + getType(new Date()) // "Date" + getType(/regex/) // "RegExp" + getType(new Map()) // "Map" + getType(new Set()) // "Set" + getType(Promise.resolve()) // "Promise" + getType(function(){}) // "Function" + getType(42) // "Number" + getType("hello") // "String" + getType(Symbol()) // "Symbol" + getType(42n) // "BigInt" + ``` + </Tab> + + <Tab title="Custom Type Checker"> + A comprehensive type-checking utility: + + ```javascript + function getType(value) { + // Handle null specially (typeof bug) + if (value === null) return "null"; + + // Handle primitives + const type = typeof value; + if (type !== "object" && type !== "function") { + return type; + } + + // Handle objects with Object.prototype.toString + const tag = Object.prototype.toString.call(value); + return tag.slice(8, -1).toLowerCase(); + } + + // Usage + getType(null) // "null" + getType([]) // "array" + getType({}) // "object" + getType(new Date()) // "date" + getType(/regex/) // "regexp" + getType(new Map()) // "map" + getType(Promise.resolve()) // "promise" + ``` + </Tab> +</Tabs> + +--- + +## Decision Guide: Which to Use? + +### The Simple Rule + +<Info> +**Default to `===`** for all comparisons. It's predictable, doesn't perform type coercion, and will save you from countless bugs. + +The only exception: Use `== null` to check for both `null` and `undefined` in one comparison. +</Info> + +### Decision Flowchart + +``` + Need to compare two values? + │ + ▼ + ┌───────────────────────────────┐ + │ Checking for null/undefined? │ + └───────────────────────────────┘ + │ │ + YES NO + │ │ + ▼ ▼ + ┌──────────┐ ┌───────────────────┐ + │ == null │ │ Need NaN or ±0? │ + └──────────┘ └───────────────────┘ + │ │ + YES NO + │ │ + ▼ ▼ + ┌──────────┐ ┌─────────┐ + │Object.is │ │ === │ + │ or │ └─────────┘ + │Number. │ + │ isNaN() │ + └──────────┘ +``` + +### Quick Reference + +| Scenario | Use | Example | +|----------|-----|---------| +| Default comparison | `===` | `if (x === 5)` | +| Check nullish | `== null` | `if (value == null)` | +| Check NaN | `Number.isNaN()` | `if (Number.isNaN(x))` | +| Check array | `Array.isArray()` | `if (Array.isArray(x))` | +| Check type | `typeof` | `if (typeof x === "string")` | +| Distinguish ±0 | `Object.is()` | `Object.is(x, -0)` | + +### ESLint Configuration + +Most style guides enforce `===` with an exception for null checks: + +```javascript +// .eslintrc.js +module.exports = { + rules: { + // Require === and !== except for null comparisons + "eqeqeq": ["error", "always", { "null": "ignore" }] + } +}; +``` + +This allows: +```javascript +// Allowed +if (value === 5) { } // Using === +if (value == null) { } // Exception for null + +// Error +if (value == 5) { } // Should use === +``` + +--- + +## Common Gotchas and Mistakes + +These common mistakes trip up many JavaScript developers. Learning about them now will save you debugging time later: + +<AccordionGroup> + <Accordion title="1. Comparing Objects by Value"> + **The mistake:** + ```javascript + const user1 = { name: "Alice" }; + const user2 = { name: "Alice" }; + + if (user1 === user2) { + console.log("Same user!"); // Never runs! + } + ``` + + **Why it's wrong:** Objects are compared by reference, not by value. Two objects with identical content are still different objects. + + **The fix:** + ```javascript + // Option 1: Compare specific properties + if (user1.name === user2.name) { + console.log("Same name!"); + } + + // Option 2: JSON.stringify (simple objects only) + if (JSON.stringify(user1) === JSON.stringify(user2)) { + console.log("Same content!"); + } + + // Option 3: Deep equality function or library + import { isEqual } from 'lodash'; + if (isEqual(user1, user2)) { + console.log("Same content!"); + } + ``` + </Accordion> + + <Accordion title="2. Truthy/Falsy Confusion with =="> + **The mistake:** + ```javascript + // These all behave unexpectedly + if ([] == false) { } // true! (but [] is truthy) + if ("0" == false) { } // true! (but "0" is truthy) + if (" " == false) { } // false (but " " is truthy) + ``` + + **Why it's confusing:** The `==` operator doesn't check truthiness. It performs type coercion according to specific rules. + + **The fix:** + ```javascript + // Use === for explicit comparisons + if (value === false) { } // Only true for actual false + + // Or check truthiness directly + if (!value) { } // Falsy check + if (value) { } // Truthy check + + // For explicit boolean conversion + if (Boolean(value) === false) { } + ``` + </Accordion> + + <Accordion title="3. NaN Comparisons"> + **The mistake:** + ```javascript + const result = parseInt("hello"); + + if (result === NaN) { + console.log("Not a number!"); // Never runs! + } + ``` + + **Why it's wrong:** `NaN` is never equal to anything, including itself. + + **The fix:** + ```javascript + // Use Number.isNaN() + if (Number.isNaN(result)) { + console.log("Not a number!"); // Works! + } + + // Or Object.is() + if (Object.is(result, NaN)) { + console.log("Not a number!"); // Works! + } + + // Avoid isNaN() - it coerces first + isNaN("hello") // true (coerces to NaN) + Number.isNaN("hello") // false (no coercion) + ``` + </Accordion> + + <Accordion title="4. The typeof null Trap"> + **The mistake:** + ```javascript + function processObject(obj) { + if (typeof obj === "object") { + // Might be null! + console.log(obj.property); // TypeError if null! + } + } + + processObject(null); // Crashes! + ``` + + **Why it's wrong:** `typeof null === "object"` is true due to a historical bug. + + **The fix:** + ```javascript + function processObject(obj) { + // Check for null AND typeof + if (obj !== null && typeof obj === "object") { + console.log(obj.property); + } + + // Or use optional chaining (ES2020) + console.log(obj?.property); + } + ``` + </Accordion> + + <Accordion title="5. String Comparison Gotchas"> + **The mistake:** + ```javascript + // Comparing numbers as strings + console.log("10" > "9"); // false! (string comparison) + + // Why? Strings compare character by character + // "1" (code 49) < "9" (code 57) + ``` + + **Why it's wrong:** String comparison uses lexicographic order (like a dictionary), not numeric value. + + **The fix:** + ```javascript + // Convert to numbers first + console.log(Number("10") > Number("9")); // true + console.log(+"10" > +"9"); // true (unary +) + console.log(parseInt("10") > parseInt("9")); // true + + // For sorting arrays of number strings + ["10", "9", "2"].sort((a, b) => a - b); // ["2", "9", "10"] + ``` + </Accordion> + + <Accordion title="6. Empty Array Comparisons"> + **The mistake:** + ```javascript + const arr = []; + + // These seem contradictory + console.log(arr == false); // true + console.log(arr ? "yes" : "no"); // "yes" + + // So arr equals false but is truthy?! + ``` + + **Why it's confusing:** `==` uses type coercion (`[] → "" → 0`), but truthiness just checks if the value is truthy (all objects are truthy). + + **The fix:** + ```javascript + // Check array length for "emptiness" + if (arr.length === 0) { + console.log("Array is empty"); + } + + // Or use the array itself as a boolean + // (but remember, empty array is truthy!) + if (!arr.length) { + console.log("Array is empty"); + } + ``` + </Accordion> +</AccordionGroup> + +--- + +## Common Misconceptions + +<AccordionGroup> + <Accordion title="Misconception 1: '== is always bad and should never be used'"> + **Not quite!** While `===` should be your default, there's one legitimate use case for `==`: + + ```javascript + // The one acceptable use of == + if (value == null) { + // Catches both null AND undefined + } + + // Equivalent to: + if (value === null || value === undefined) { + // Same result, but more verbose + } + ``` + + This is cleaner than checking for both values separately and is explicitly allowed by most style guides (including ESLint's `eqeqeq` rule with the `"null": "ignore"` option). + </Accordion> + + <Accordion title="Misconception 2: '=== checks if types are the same'"> + **Partially wrong!** `===` doesn't *just* check types. It checks if two values are the **same type AND same value**. + + ```javascript + // Same type, different values → false + 5 === 10 // false (both numbers, different values) + "hello" === "hi" // false (both strings, different values) + + // Different types → immediately false + 5 === "5" // false (no value comparison even attempted) + ``` + + The key point: `===` returns `false` immediately if types differ, then compares values if types match. + </Accordion> + + <Accordion title="Misconception 3: 'typeof is reliable for checking all types'"> + **Wrong!** `typeof` has several well-known quirks: + + ```javascript + typeof null // "object" — famous bug from 1995! + typeof [] // "object" — arrays are objects + typeof NaN // "number" — Not-a-Number is a number type + typeof function(){} // "function" — but functions ARE objects! + ``` + + **Better alternatives:** + - Use `Array.isArray()` for arrays + - Use `value === null` for null + - Use `Number.isNaN()` for NaN + - Use `Object.prototype.toString.call()` for precise type detection + </Accordion> + + <Accordion title="Misconception 4: 'Objects with the same content are equal'"> + **Wrong!** Objects (including arrays and functions) are compared by **reference**, not by content: + + ```javascript + { a: 1 } === { a: 1 } // false — different objects in memory! + [] === [] // false — different arrays! + (() => {}) === (() => {}) // false — different functions! + + const obj = { a: 1 }; + obj === obj // true — same reference + ``` + + To compare object contents, use `JSON.stringify()` for simple cases or a deep equality function like Lodash's `_.isEqual()`. + </Accordion> + + <Accordion title="Misconception 5: 'NaN means the value is not a number'"> + **Misleading!** `NaN` is actually a *numeric value* that represents an undefined or unrepresentable mathematical result: + + ```javascript + typeof NaN // "number" — NaN IS a number type! + + // NaN appears from invalid math operations + 0 / 0 // NaN + Math.sqrt(-1) // NaN + parseInt("xyz") // NaN + Infinity - Infinity // NaN + ``` + + Think of `NaN` as "the result of a calculation that doesn't produce a meaningful number" rather than literally "not a number." + </Accordion> + + <Accordion title="Misconception 6: 'Truthy values are == true'"> + **Wrong!** Truthy/falsy and `==` equality are completely different concepts: + + ```javascript + // These are truthy but NOT == true + "hello" == true // false! ("hello" → NaN, true → 1) + 2 == true // false! (2 !== 1) + [] == true // false! ([] → "" → 0, true → 1) + + // But they ARE truthy + if ("hello") { } // executes + if (2) { } // executes + if ([]) { } // executes + ``` + + **Rule:** Don't use `== true` or `== false`. Either use `===` or just rely on truthiness directly: `if (value)`. + </Accordion> +</AccordionGroup> + +--- + +## Key Takeaways + +<Info> +**The key things to remember about Equality Operators:** + +1. **Use `===` by default** — It's predictable and doesn't convert types + +2. **`==` converts types first** — This leads to unexpected results like `"0" == false` being `true` + +3. **Only use `==` for null checks** — `value == null` checks for both `null` and `undefined` + +4. **`NaN !== NaN`** — NaN doesn't equal anything, not even itself. Use `Number.isNaN()` to check for it + +5. **Objects compare by reference** — `{} === {}` is `false` because they're different objects in memory + +6. **`typeof null === "object"`** — This is a bug that can't be fixed. Always check for `null` directly + +7. **`Object.is()` for edge cases** — Use it when you need to check for `NaN` or distinguish `+0` from `-0` + +8. **Arrays return `"object"` from typeof** — Use `Array.isArray()` to check for arrays + +9. **These rules are commonly asked in interviews** — Now you're prepared! + +10. **Configure ESLint** — Use the `eqeqeq` rule to enforce `===` in your projects +</Info> + +--- + +## Interactive Visualization Tool + +The best way to internalize JavaScript's equality rules is to see all the comparisons at once. + +<Card title="JavaScript Equality Table" icon="table" href="https://dorey.github.io/JavaScript-Equality-Table/"> + Interactive comparison table by dorey showing the results of `==` and `===` for all type combinations. Hover over cells to see explanations. An essential reference for understanding JavaScript equality! +</Card> + +**Try these in the table:** +- Compare `[]` with `false`, `0`, `""`, and `![]` to see why `[] == ![]` is `true` +- See why `null == undefined` is `true` but neither equals `0` or `false` +- Observe how `NaN` never equals anything (including itself) +- Notice how objects only equal themselves (same reference) + +<Tip> +**Bookmark this table!** It's invaluable for debugging comparison issues and preparing for technical interviews. +</Tip> + +--- + +## Test Your Knowledge + +Try to answer each question before revealing the solution: + +<AccordionGroup> + <Accordion title="Question 1: What is the output of [] == ![] ?"> + **Answer:** `true` + + **Step-by-step:** + 1. `![]` → `false` (arrays are truthy, so negation makes false) + 2. `[] == false` → `[] == 0` (boolean converts to number) + 3. `[] == 0` → `"" == 0` (array converts to empty string) + 4. `"" == 0` → `0 == 0` (string converts to number) + 5. `0 == 0` → `true` + + This is why understanding type conversion is so important! + </Accordion> + + <Accordion title="Question 2: Why does typeof null return 'object'?"> + **Answer:** This is a bug from JavaScript's original implementation in 1995. + + In the original C code, values were represented with a type tag. Objects had the tag `000`, and `null` was represented as the NULL pointer (`0x00`), which also matched the `000` tag for objects. + + This bug was never fixed because too much existing code depends on this behavior. A proposal to fix it was rejected for backward compatibility reasons. + + **Workaround:** Always check for null explicitly: `value === null` + </Accordion> + + <Accordion title="Question 3: How would you properly check if a value is NaN?"> + **Answer:** Use `Number.isNaN()`: + + ```javascript + Number.isNaN(NaN) // true + Number.isNaN("hello") // false (no coercion) + Number.isNaN(undefined) // false + ``` + + **Avoid** the global `isNaN()` because it coerces its argument first: + + ```javascript + isNaN("hello") // true (coerces to NaN) + isNaN(undefined) // true (coerces to NaN) + ``` + + You can also use `Object.is(value, NaN)` which returns `true` for `NaN`. + </Accordion> + + <Accordion title="Question 4: What's the ONE legitimate use case for ==?"> + **Answer:** Checking for both `null` and `undefined` in a single comparison: + + ```javascript + // Using == + if (value == null) { + // value is null OR undefined + } + + // Equivalent to: + if (value === null || value === undefined) { + // value is null OR undefined + } + ``` + + This works because `null == undefined` is `true` (special case in the spec), but `null` and `undefined` don't loosely equal anything else (not even `0`, `""`, or `false`). + </Accordion> + + <Accordion title="Question 5: Why does {} === {} return false?"> + **Answer:** Objects are compared by **reference**, not by **value**. + + When you write `{}`, JavaScript creates a new object in memory. When you write another `{}`, it creates a completely different object. Even though they have the same content (both are empty objects), they are stored at different memory locations. + + ```javascript + const a = {}; + const b = {}; + const c = a; + + a === b // false (different objects) + a === c // true (same reference) + ``` + + To compare objects by content, you need to compare their properties or use a deep equality function. + </Accordion> + + <Accordion title="Question 6: What's the difference between === and Object.is()?"> + **Answer:** They behave identically except for two edge cases: + + | Expression | `===` | `Object.is()` | + |------------|-------|---------------| + | `NaN, NaN` | `false` | `true` | + | `+0, -0` | `true` | `false` | + + ```javascript + NaN === NaN // false + Object.is(NaN, NaN) // true + + +0 === -0 // true + Object.is(+0, -0) // false + ``` + + Use `===` for everyday comparisons. Use `Object.is()` when you specifically need to check for `NaN` equality or distinguish positive from negative zero. + </Accordion> + + <Accordion title="Question 7: How would you reliably check if something is an array?"> + **Answer:** Use `Array.isArray()`: + + ```javascript + Array.isArray([]) // true + Array.isArray([1, 2, 3]) // true + Array.isArray(new Array()) // true + + Array.isArray({}) // false + Array.isArray("hello") // false + Array.isArray(null) // false + ``` + + **Why not `typeof`?** Because `typeof [] === "object"`. Arrays are objects in JavaScript. + + **Why not `instanceof Array`?** It works in most cases, but can fail across different JavaScript realms (like iframes) where each realm has its own `Array` constructor. + </Accordion> +</AccordionGroup> + +--- + +## Related Concepts + +<CardGroup cols={2}> + <Card title="Type Coercion" icon="shuffle" href="/concepts/type-coercion"> + Deep dive into how JavaScript converts between types automatically + </Card> + <Card title="Primitive Types" icon="cube" href="/concepts/primitive-types"> + Understanding JavaScript's fundamental data types + </Card> + <Card title="Value Types vs Reference Types" icon="clone" href="/concepts/value-reference-types"> + How primitives and objects are stored differently in memory + </Card> + <Card title="Scope and Closures" icon="layer-group" href="/concepts/scope-and-closures"> + Understanding where variables are accessible in your code + </Card> +</CardGroup> + +--- + +## Reference + +<CardGroup cols={2}> + <Card title="Equality comparisons and sameness — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness"> + Comprehensive official documentation covering ==, ===, Object.is(), and SameValue + </Card> + <Card title="typeof — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof"> + Official documentation on the typeof operator and its behavior + </Card> +</CardGroup> + +--- + +## Articles + +<CardGroup cols={2}> + <Card title="JavaScript Double Equals vs. Triple Equals — Brandon Morelli" icon="newspaper" href="https://codeburst.io/javascript-double-equals-vs-triple-equals-61d4ce5a121a"> + Uses side-by-side code comparisons to show exactly when == and === produce different results. Great starting point if you're new to JavaScript equality. + </Card> + <Card title="What is the difference between == and === in JavaScript? — Craig Buckler" icon="newspaper" href="https://www.oreilly.com/learning/what-is-the-difference-between-and-in-javascript"> + O'Reilly's take on the equality debate with a clear recommendation on which operator to default to. Includes the edge cases that trip up even experienced developers. + </Card> + <Card title="=== vs == Comparison in JavaScript — FreeCodeCamp" icon="newspaper" href="https://www.freecodecamp.org/news/javascript-triple-equals-sign-vs-double-equals-sign-comparison-operators-explained-with-examples/"> + Walks through the type coercion algorithm step-by-step with dozens of examples. The boolean comparison section explains why `true == "true"` returns false. + </Card> + <Card title="Checking Types in Javascript — Toby Ho" icon="newspaper" href="http://tobyho.com/2011/01/28/checking-types-in-javascript/"> + Covers the limitations of typeof and when to use instanceof, Object.prototype.toString, or duck typing instead. Includes a reusable type-checking utility function. + </Card> + <Card title="How to better check data types in JavaScript — Webbjocke" icon="newspaper" href="https://webbjocke.com/javascript-check-data-types/"> + Provides copy-paste utility functions for checking arrays, objects, nulls, and primitives. Explains why each approach works and when to use which method. + </Card> + <Card title="JavaScript Equality Table — dorey" icon="newspaper" href="https://dorey.github.io/JavaScript-Equality-Table/"> + Visual comparison table showing the results of == and === for all type combinations. Essential reference! + </Card> +</CardGroup> + +--- + +## Videos + +<CardGroup cols={2}> + <Card title='JavaScript "==" VS "===" — Web Dev Simplified' icon="video" href="https://www.youtube.com/watch?v=C5ZVC4HHgIg"> + 8-minute breakdown with on-screen code examples showing type coercion in action. Kyle's explanation of the null/undefined special case is particularly helpful. + </Card> + <Card title="JavaScript - The typeof operator — Java Brains" icon="video" href="https://www.youtube.com/watch?v=ol_su88I3kw"> + Demonstrates the typeof null bug and explains why it exists. Shows how to build a reliable type-checking function that handles all edge cases. + </Card> + <Card title="=== vs == in JavaScript — Hitesh Choudhary" icon="video" href="https://www.youtube.com/watch?v=a0S1iG3TgP0"> + Live coding session showing surprising equality results and debugging them in the console. Great for seeing how these operators behave in real development. + </Card> + <Card title="== ? === ??? ...#@^% — Shirmung Bielefeld" icon="video" href="https://www.youtube.com/watch?v=qGyqzN0bjhc&t"> + Conference talk diving deep into JavaScript's equality quirks and type coercion weirdness. + </Card> +</CardGroup> + +--- + +## Books + +<Card title="You Don't Know JS: Types & Grammar — Kyle Simpson" icon="book" href="https://github.com/getify/You-Dont-Know-JS/blob/2nd-ed/types-grammar/README.md"> + The definitive deep-dive into JavaScript types, coercion, and equality. Free to read online. Essential reading for truly understanding how JavaScript handles comparisons. +</Card> diff --git a/docs/concepts/error-handling.mdx b/docs/concepts/error-handling.mdx new file mode 100644 index 00000000..f3f52142 --- /dev/null +++ b/docs/concepts/error-handling.mdx @@ -0,0 +1,999 @@ +--- +title: "Error Handling: Managing Errors Gracefully in JavaScript" +sidebarTitle: "Error Handling: Managing Errors Gracefully" +description: "Learn JavaScript error handling with try/catch/finally. Understand Error types, custom errors, async error patterns, and best practices for robust code." +--- + +What happens when something goes wrong in your JavaScript code? How do you prevent one small error from crashing your entire application? How do you give users helpful feedback instead of a cryptic error message? + +```javascript +// Without error handling - your app crashes +const userData = JSON.parse('{ invalid json }') // SyntaxError! + +// With error handling - you stay in control +try { + const userData = JSON.parse('{ invalid json }') +} catch (error) { + console.log('Could not parse user data:', error.message) + // Show user a friendly message, use default data, etc. +} +``` + +**[Error handling](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Control_flow_and_error_handling#exception_handling_statements)** is how you detect, respond to, and recover from errors in your code. JavaScript provides the `try...catch...finally` statement for synchronous errors and special patterns for handling async errors in [Promises](/concepts/promises) and [async/await](/concepts/async-await). + +<Info> +**What you'll learn in this guide:** +- The `try...catch...finally` statement and when to use each block +- The Error object and its properties (name, message, stack) +- Built-in Error types: TypeError, ReferenceError, SyntaxError, and more +- How to throw your own errors with meaningful messages +- Creating custom Error classes for better error categorization +- Error handling patterns for async code +- Global error handlers for catching uncaught errors +- Common mistakes and real-world patterns +</Info> + +<Warning> +**Helpful prerequisite:** This guide covers async error handling briefly. For a deeper dive into async patterns, check out [Promises](/concepts/promises) and [async/await](/concepts/async-await) first. +</Warning> + +--- + +## What is Error Handling in JavaScript? + +Errors happen. Users enter invalid data, network requests fail, APIs return unexpected responses, and sometimes we just make typos. **Error handling** is your strategy for detecting, responding to, and recovering from these problems gracefully. In JavaScript, you use the `try...catch` statement to catch errors, the `throw` statement to create them, and the `Error` object to describe what went wrong. + +<CardGroup cols={2}> + <Card title="Error — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error"> + Official MDN documentation for the Error object + </Card> + <Card title="try...catch — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch"> + MDN documentation for the try...catch statement + </Card> +</CardGroup> + +--- + +## The Safety Net Analogy + +Think of error handling like a trapeze act at a circus. The acrobat (your code) performs risky moves high above the ground. The safety net (your catch block) is there to catch them if they fall. And no matter what happens, the show must go on (your finally block). + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ THE SAFETY NET ANALOGY │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ try { TRAPEZE ACT │ +│ riskyMove() ┌─────────┐ │ +│ } │ ACROBAT │ ← Your risky code │ +│ └────┬────┘ │ +│ │ │ +│ catch (error) { ▼ FALLS! │ +│ recover() ═══════════════════════ │ +│ } SAFETY NET ← Catches the error │ +│ │ +│ finally { The show continues! │ +│ cleanup() (runs no matter what) │ +│ } │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +| Circus | JavaScript | Purpose | +|--------|------------|---------| +| Trapeze act | `try` block | Code that might fail | +| Safety net | `catch` block | Handles the error if one occurs | +| Show continues | `finally` block | Cleanup that always runs | +| Acrobat falls | Error is thrown | Something went wrong | + +--- + +## The try/catch/finally Statement + +The **[`try...catch`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch)** statement is JavaScript's primary tool for handling errors. Here's the full syntax: + +```javascript +try { + // Code that might throw an error + const result = riskyOperation() + console.log(result) + +} catch (error) { + // Code that runs if an error is thrown + console.error('Something went wrong:', error.message) + +} finally { + // Code that ALWAYS runs, error or not + cleanup() +} +``` + +### The try Block + +The `try` block contains code that might throw an error. If an error occurs, execution immediately jumps to the `catch` block. + +```javascript +try { + console.log('Starting...') // Runs + JSON.parse('{ bad json }') // Error! Jump to catch + console.log('This never runs') // Skipped +} +``` + +### The catch Block + +The `catch` block receives the error object and handles it. This is where you log errors, show user messages, or attempt recovery. + +```javascript +try { + const data = JSON.parse(userInput) +} catch (error) { + // error contains information about what went wrong + console.log(error.name) // "SyntaxError" + console.log(error.message) // "Unexpected token b in JSON..." + + // You can recover gracefully + const data = { fallback: true } +} +``` + +<Tip> +**Optional catch binding:** If you don't need the error object, you can omit it (ES2019+): + +```javascript +try { + JSON.parse(maybeJson) +} catch { + // No (error) parameter needed if you don't use it + return null +} +``` +</Tip> + +### The finally Block + +The `finally` block always runs, whether an error occurred or not. It's perfect for cleanup code like closing connections or hiding loading spinners. + +```javascript +let isLoading = true + +try { + const data = await fetchData() + displayData(data) +} catch (error) { + showErrorMessage(error) +} finally { + // This runs no matter what! + isLoading = false + hideLoadingSpinner() +} +``` + +<Warning> +**finally runs even with return:** If you return from a try or catch block, finally still executes before the function returns: + +```javascript +function example() { + try { + return 'from try' + } finally { + console.log('finally runs!') // This still logs! + } +} + +example() // Logs "finally runs!", then returns "from try" +``` +</Warning> + +### try/catch Only Works Synchronously + +This trips people up: `try/catch` won't catch errors in callbacks that run later. + +```javascript +// ❌ WRONG - catch won't catch this error! +try { + setTimeout(() => { + throw new Error('Async error') + }, 1000) +} catch (error) { + console.log('This never runs') +} + +// ✓ CORRECT - try/catch inside the callback +setTimeout(() => { + try { + throw new Error('Async error') + } catch (error) { + console.log('Caught:', error.message) + } +}, 1000) +``` + +For async code, see the [Async Error Handling](#async-error-handling) section. + +--- + +## The Error Object + +When an error occurs, JavaScript creates an **[Error](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error)** object with information about what went wrong. + +### Error Properties + +| Property | Description | Example | +|----------|-------------|---------| +| `name` | The type of error | `"TypeError"`, `"ReferenceError"` | +| `message` | Human-readable description | `"Cannot read property 'x' of undefined"` | +| `stack` | Call stack when error occurred (non-standard but widely supported) | Shows file names, line numbers | +| `cause` | Original error (ES2022+) | Used for error chaining | + +```javascript +try { + undefinedVariable +} catch (error) { + console.log(error.name) // "ReferenceError" + console.log(error.message) // "undefinedVariable is not defined" + console.log(error.stack) // Full stack trace with line numbers +} +``` + +The `stack` property is essential for debugging. It shows exactly where the error occurred and the chain of function calls that led to it. + +--- + +## Built-in Error Types + +JavaScript has several built-in error types. Knowing them helps you understand what went wrong and how to fix it. + +| Error Type | When It Occurs | Common Cause | +|------------|----------------|--------------| +| **[Error](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error)** | Generic error | Base class, used for custom errors | +| **[TypeError](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypeError)** | Wrong type | `null.foo`, calling non-function | +| **[ReferenceError](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ReferenceError)** | Invalid reference | Using undefined variable | +| **[SyntaxError](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SyntaxError)** | Invalid syntax | Bad JSON, missing brackets | +| **[RangeError](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RangeError)** | Value out of range | `new Array(-1)` | +| **[URIError](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/URIError)** | Bad URI encoding | `decodeURIComponent('%')` | +| **[AggregateError](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AggregateError)** | Multiple errors | `Promise.any()` all reject | + +<AccordionGroup> + <Accordion title="TypeError - The most common error"> + Occurs when a value is not the expected type, like calling a method on `null` or `undefined`: + + ```javascript + const user = null + console.log(user.name) // TypeError: Cannot read property 'name' of null + + const notAFunction = 42 + notAFunction() // TypeError: notAFunction is not a function + ``` + + **Fix:** Check if values exist before using them: + ```javascript + console.log(user?.name) // undefined (no error) + ``` + </Accordion> + + <Accordion title="ReferenceError - Variable doesn't exist"> + Occurs when you try to use a variable that hasn't been declared: + + ```javascript + console.log(userName) // ReferenceError: userName is not defined + ``` + + **Common causes:** Typos in variable names, forgetting to import, using variables before declaration. + </Accordion> + + <Accordion title="SyntaxError - Invalid code or JSON"> + Occurs when code has invalid syntax or when parsing invalid JSON: + + ```javascript + JSON.parse('{ name: "John" }') // SyntaxError: Unexpected token n + // JSON requires double quotes: { "name": "John" } + + JSON.parse('') // SyntaxError: Unexpected end of JSON input + ``` + + **Note:** Syntax errors in your source code are caught at parse time, not runtime. `try/catch` only catches runtime SyntaxErrors like invalid JSON. + </Accordion> + + <Accordion title="RangeError - Value out of bounds"> + Occurs when a value is outside its allowed range: + + ```javascript + new Array(-1) // RangeError: Invalid array length + (1.5).toFixed(200) // RangeError: precision out of range (max is 100) + 'x'.repeat(Infinity) // RangeError: Invalid count value + ``` + </Accordion> +</AccordionGroup> + +--- + +## The throw Statement + +The **[`throw`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/throw)** statement lets you create your own errors. When you throw, execution stops and jumps to the nearest catch block. + +```javascript +function divide(a, b) { + if (b === 0) { + throw new Error('Cannot divide by zero') + } + return a / b +} + +try { + const result = divide(10, 0) +} catch (error) { + console.log(error.message) // "Cannot divide by zero" +} +``` + +### Always Throw Error Objects + +Technically you can throw anything, but always throw Error objects. They include a stack trace for debugging. + +```javascript +// ❌ BAD - No stack trace, hard to debug +throw 'Something went wrong' +throw 404 +throw { message: 'Error' } + +// ✓ GOOD - Includes stack trace +throw new Error('Something went wrong') +throw new TypeError('Expected a string') +throw new RangeError('Value must be between 0 and 100') +``` + +### Creating Meaningful Error Messages + +Good error messages tell you what went wrong and ideally how to fix it: + +```javascript +// ❌ Vague +throw new Error('Invalid input') + +// ✓ Specific +throw new Error('Email address is invalid: missing @ symbol') +throw new TypeError(`Expected string but got ${typeof value}`) +throw new RangeError(`Age must be between 0 and 150, got ${age}`) +``` + +--- + +## Custom Error Classes + +For larger applications, create custom error classes to categorize errors and add extra information. + +```javascript +class ValidationError extends Error { + constructor(message) { + super(message) + this.name = 'ValidationError' + } +} + +class NetworkError extends Error { + constructor(message, statusCode) { + super(message) + this.name = 'NetworkError' + this.statusCode = statusCode + } +} +``` + +### The Auto-Naming Pattern + +Instead of manually setting `this.name` in every class, use the constructor name: + +```javascript +class AppError extends Error { + constructor(message, options) { + super(message, options) + this.name = this.constructor.name // Automatically uses class name + } +} + +class ValidationError extends AppError {} +class DatabaseError extends AppError {} +class NetworkError extends AppError {} + +// All have correct names automatically +throw new ValidationError('Invalid email') // error.name === "ValidationError" +``` + +### Using instanceof for Error Handling + +Custom errors let you handle different error types differently: + +```javascript +try { + await saveUser(userData) +} catch (error) { + if (error instanceof ValidationError) { + // Show validation message to user + showFieldErrors(error.fields) + } else if (error instanceof NetworkError) { + // Network issue - maybe retry + showRetryButton() + } else { + // Unknown error - log and show generic message + console.error('Unexpected error:', error) + showGenericError() + } +} +``` + +### Error Chaining with cause (ES2022+) + +When catching and re-throwing errors, preserve the original error using the `cause` option: + +```javascript +async function fetchUserData(userId) { + try { + const response = await fetch(`/api/users/${userId}`) + return await response.json() + } catch (error) { + // Wrap the original error with more context + throw new Error(`Failed to load user ${userId}`, { cause: error }) + } +} + +// Later, you can access the original error +try { + await fetchUserData(123) +} catch (error) { + console.log(error.message) // "Failed to load user 123" + console.log(error.cause.message) // Original fetch error +} +``` + +--- + +## Async Error Handling + +Error handling works differently with asynchronous code. Here's a quick overview. For comprehensive coverage, see our [Promises](/concepts/promises) and [async/await](/concepts/async-await) guides. + +### With Promises: .catch() + +Use `.catch()` to handle errors in Promise chains: + +```javascript +fetch('/api/users') + .then(response => response.json()) + .then(users => displayUsers(users)) + .catch(error => { + // Catches errors from fetch, json parsing, or displayUsers + console.error('Failed to load users:', error) + }) + .finally(() => { + hideLoadingSpinner() + }) +``` + +### With async/await: try/catch + +With async/await, use regular try/catch blocks: + +```javascript +async function loadUsers() { + try { + const response = await fetch('/api/users') + const users = await response.json() + return users + } catch (error) { + console.error('Failed to load users:', error) + throw error // Re-throw if caller should handle it + } +} +``` + +### The fetch() Trap: Check response.ok + +This catches many developers off guard: **`fetch()` doesn't throw on HTTP errors** like 404 or 500. It only throws on network failures. + +```javascript +// ❌ WRONG - This won't catch 404 or 500 errors! +try { + const response = await fetch('/api/users/999') + const user = await response.json() // Might fail on error response +} catch (error) { + // Only catches network errors, not HTTP errors +} + +// ✓ CORRECT - Check response.ok +try { + const response = await fetch('/api/users/999') + + if (!response.ok) { + throw new Error(`HTTP error: ${response.status}`) + } + + const user = await response.json() +} catch (error) { + // Now catches both network AND HTTP errors + console.error('Request failed:', error.message) +} +``` + +<Warning> +**The #1 async mistake:** Using `forEach` with async callbacks doesn't work as expected. Use `for...of` for sequential or `Promise.all` for parallel. See our [async/await guide](/concepts/async-await) for details. +</Warning> + +--- + +## Global Error Handlers + +Global error handlers catch errors that slip through your try/catch blocks. They're a safety net of last resort, not a replacement for proper error handling. + +### window.onerror - Synchronous Errors + +Catches uncaught errors in the browser: + +```javascript +window.onerror = function(message, source, lineno, colno, error) { + console.log('Uncaught error:', message) + console.log('Source:', source, 'Line:', lineno) + + // Send to error tracking service + logErrorToService(error) + + // Return true to prevent default browser error handling + return true +} +``` + +### unhandledrejection - Promise Rejections + +Catches unhandled Promise rejections: + +```javascript +window.addEventListener('unhandledrejection', event => { + console.warn('Unhandled promise rejection:', event.reason) + + // Prevent the default browser warning + event.preventDefault() + + // Log to error tracking service + logErrorToService(event.reason) +}) +``` + +<Tip> +**When to use global handlers:** +- Logging errors to a service like Sentry or LogRocket +- Showing a generic "something went wrong" message +- Tracking errors in production + +**Not for:** Regular error handling. Always prefer specific try/catch blocks. +</Tip> + +--- + +## Common Mistakes + +### Mistake 1: Empty catch Blocks (Swallowing Errors) + +```javascript +// ❌ WRONG - Error is silently lost +try { + riskyOperation() +} catch (error) { + // Nothing here - you'll never know something failed +} + +// ✓ CORRECT - At minimum, log the error +try { + riskyOperation() +} catch (error) { + console.error('Operation failed:', error) +} +``` + +### Mistake 2: Catching Too Broadly + +```javascript +// ❌ WRONG - Hides programming bugs +try { + processData(data) + undefinedVriable // Typo! This bug is now hidden +} catch (error) { + return 'Something went wrong' +} + +// ✓ CORRECT - Only catch expected errors +try { + return JSON.parse(userInput) +} catch (error) { + if (error instanceof SyntaxError) { + return null // Expected: invalid JSON + } + throw error // Unexpected: re-throw +} +``` + +### Mistake 3: Throwing Strings Instead of Errors + +```javascript +// ❌ WRONG - No stack trace +throw 'User not found' + +// ✓ CORRECT - Has stack trace for debugging +throw new Error('User not found') +``` + +### Mistake 4: Not Re-throwing When Needed + +```javascript +// ❌ WRONG - Caller doesn't know an error occurred +async function fetchData() { + try { + return await fetch('/api/data') + } catch (error) { + console.log('Error:', error) + // Returns undefined - caller thinks it succeeded! + } +} + +// ✓ CORRECT - Re-throw or return meaningful value +async function fetchData() { + try { + return await fetch('/api/data') + } catch (error) { + console.log('Error:', error) + throw error // Let caller handle it + // OR: return null with explicit meaning + } +} +``` + +### Mistake 5: Forgetting try/catch is Synchronous + +```javascript +// ❌ WRONG - Won't catch async errors +try { + setTimeout(() => { + throw new Error('Async error') // Uncaught! + }, 1000) +} catch (error) { + console.log('Never runs') +} + +// ✓ CORRECT - Put try/catch inside callback +setTimeout(() => { + try { + throw new Error('Async error') + } catch (error) { + console.log('Caught:', error.message) + } +}, 1000) +``` + +--- + +## Real-World Patterns + +### Retry Pattern + +Automatically retry failed operations, useful for flaky network requests: + +```javascript +async function fetchWithRetry(url, retries = 3) { + for (let i = 0; i < retries; i++) { + try { + const response = await fetch(url) + if (!response.ok) throw new Error(`HTTP ${response.status}`) + return await response.json() + } catch (error) { + if (i === retries - 1) throw error // Last attempt, give up + + // Wait before retrying (exponential backoff) + await new Promise(r => setTimeout(r, 1000 * Math.pow(2, i))) + } + } +} +``` + +### Validation Error Pattern + +Collect multiple validation errors at once: + +```javascript +class ValidationError extends Error { + constructor(errors) { + super('Validation failed') + this.name = 'ValidationError' + this.errors = errors // { email: "Invalid email", age: "Must be positive" } + } +} + +function validateUser(data) { + const errors = {} + + if (!data.email?.includes('@')) { + errors.email = 'Invalid email address' + } + if (data.age < 0) { + errors.age = 'Age must be positive' + } + + if (Object.keys(errors).length > 0) { + throw new ValidationError(errors) + } +} + +// Usage +try { + validateUser({ email: 'bad', age: -5 }) +} catch (error) { + if (error instanceof ValidationError) { + // Show errors next to form fields + Object.entries(error.errors).forEach(([field, message]) => { + showFieldError(field, message) + }) + } +} +``` + +### Graceful Degradation + +Try the ideal path, fall back to alternatives: + +```javascript +async function loadUserPreferences(userId) { + try { + // Try to fetch from API + return await fetchFromApi(`/preferences/${userId}`) + } catch (apiError) { + console.warn('API unavailable, trying cache:', apiError.message) + + try { + // Fall back to local storage + const cached = localStorage.getItem(`prefs_${userId}`) + if (cached) return JSON.parse(cached) + } catch (cacheError) { + console.warn('Cache unavailable:', cacheError.message) + } + + // Fall back to defaults + return { theme: 'light', language: 'en' } + } +} +``` + +--- + +## Key Takeaways + +<Info> +**The key things to remember:** + +1. **Use try/catch for synchronous code** — Wrap risky operations and handle errors appropriately + +2. **try/catch is synchronous** — It won't catch errors in callbacks. Use `.catch()` for Promises or try/catch inside async functions + +3. **Always throw Error objects, not strings** — Error objects include stack traces that are essential for debugging + +4. **Always check response.ok with fetch** — `fetch()` doesn't throw on HTTP errors like 404 or 500 + +5. **Create custom Error classes** — They help categorize errors and add context for better handling + +6. **Use finally for cleanup** — Code in finally always runs, perfect for hiding spinners or closing connections + +7. **Don't swallow errors** — Empty catch blocks hide bugs. Always log or re-throw + +8. **Use error.cause for chaining** — Preserve original errors when wrapping them with more context + +9. **Re-throw errors you can't handle** — If you catch an error you didn't expect, re-throw it + +10. **Use global handlers as a safety net** — They're for logging and tracking, not for regular error handling +</Info> + +--- + +## Test Your Knowledge + +<AccordionGroup> + <Accordion title="Question 1: What's the difference between try/catch and Promise .catch()?"> + **Answer:** + + `try/catch` only catches **synchronous** errors. If you have async code inside the try block (like setTimeout callbacks), errors won't be caught. + + Promise `.catch()` catches **Promise rejections**, which are async. With async/await, you can use try/catch because `await` converts rejections to thrown errors. + + ```javascript + // try/catch with async/await - works! + try { + await fetch('/api/data') + } catch (error) { + // Catches rejections because await converts them + } + + // try/catch with callbacks - doesn't work! + try { + setTimeout(() => { throw new Error() }, 1000) + } catch (error) { + // Never runs - the error is thrown later + } + ``` + </Accordion> + + <Accordion title="Question 2: Why doesn't fetch() throw on 404 or 500 errors?"> + **Answer:** + + `fetch()` only throws on **network failures** (can't reach the server). HTTP errors like 404 (Not Found) or 500 (Server Error) are valid HTTP responses, so `fetch()` resolves successfully. + + You must check `response.ok` to detect HTTP errors: + + ```javascript + const response = await fetch('/api/users/999') + + if (!response.ok) { + // 404, 500, etc. + throw new Error(`HTTP error: ${response.status}`) + } + + const data = await response.json() + ``` + </Accordion> + + <Accordion title="Question 3: Why should you throw Error objects instead of strings?"> + **Answer:** + + Error objects include a **stack trace** showing where the error occurred and the chain of function calls. Strings don't have this information. + + ```javascript + throw 'Something went wrong' // No stack trace + throw new Error('Something went wrong') // Has stack trace + ``` + + The stack trace is essential for debugging, especially in production where you can't use a debugger. + </Accordion> + + <Accordion title="Question 4: What does the finally block do?"> + **Answer:** + + The `finally` block **always runs**, whether an error occurred or not, and even if there's a `return` statement in try or catch. It's ideal for cleanup code. + + ```javascript + function example() { + try { + return 'success' + } catch (error) { + return 'error' + } finally { + console.log('Cleanup!') // Always runs! + } + } + + example() // Logs "Cleanup!" then returns "success" + ``` + + Use it for: hiding loading spinners, closing connections, releasing resources. + </Accordion> + + <Accordion title="Question 5: How do you handle different error types differently?"> + **Answer:** + + Use `instanceof` to check the error type, or check `error.name`: + + ```javascript + try { + riskyOperation() + } catch (error) { + if (error instanceof TypeError) { + console.log('Type error:', error.message) + } else if (error instanceof SyntaxError) { + console.log('Syntax error:', error.message) + } else { + // Unknown error - re-throw it + throw error + } + } + ``` + + This is especially useful with custom error classes: + + ```javascript + if (error instanceof ValidationError) { + showFormErrors(error.errors) + } else if (error instanceof NetworkError) { + showOfflineMessage() + } + ``` + </Accordion> + + <Accordion title="Question 6: What's wrong with this code?"> + ```javascript + try { + const result = riskyOperation() + } catch (e) { + // Handle error + } + + console.log(result) // ??? + ``` + + **Answer:** + + `result` is scoped to the try block. It doesn't exist outside of it, so `console.log(result)` throws a ReferenceError. + + **Fix:** Declare the variable outside the try block: + + ```javascript + let result + + try { + result = riskyOperation() + } catch (e) { + result = 'fallback value' + } + + console.log(result) // Works! + ``` + </Accordion> +</AccordionGroup> + +--- + +## Related Concepts + +<CardGroup cols={2}> + <Card title="Promises" icon="handshake" href="/concepts/promises"> + Error handling with .catch() and Promise rejection patterns + </Card> + <Card title="async/await" icon="hourglass" href="/concepts/async-await"> + Using try/catch with async functions for cleaner async error handling + </Card> + <Card title="Callbacks" icon="phone" href="/concepts/callbacks"> + Error-first callbacks: the original async error handling pattern + </Card> + <Card title="Event Loop" icon="arrows-spin" href="/concepts/event-loop"> + Understand why try/catch doesn't work with async callbacks + </Card> +</CardGroup> + +--- + +## Reference + +<CardGroup cols={2}> + <Card title="Error — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error"> + Complete reference for the Error object and its properties + </Card> + <Card title="try...catch — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch"> + Documentation for try, catch, and finally blocks + </Card> + <Card title="throw — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/throw"> + How to throw your own errors + </Card> + <Card title="Control Flow — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Control_flow_and_error_handling"> + MDN guide covering error handling in context + </Card> +</CardGroup> + +## Articles + +<CardGroup cols={2}> + <Card title="Error handling, try...catch — JavaScript.info" icon="newspaper" href="https://javascript.info/try-catch"> + The definitive guide to JavaScript error handling. Covers everything from basics to rethrowing, with clear examples and interactive exercises. + </Card> + <Card title="Custom errors, extending Error — JavaScript.info" icon="newspaper" href="https://javascript.info/custom-errors"> + Learn to create custom error classes with proper inheritance. The wrapping exceptions pattern here is essential for larger applications. + </Card> + <Card title="A Definitive Guide to Handling Errors in JavaScript — Kinsta" icon="newspaper" href="https://kinsta.com/blog/errors-in-javascript/"> + Comprehensive overview covering all error types, stack traces, and production error handling strategies with middleware examples. + </Card> +</CardGroup> + +## Videos + +<CardGroup cols={2}> + <Card title="JavaScript Error Handling — Web Dev Simplified" icon="video" href="https://www.youtube.com/watch?v=blBoIyNhGvY"> + Clear 15-minute walkthrough of try/catch/finally with practical examples. Perfect if you prefer watching code being written. + </Card> + <Card title="try, catch, finally, throw — Fireship" icon="video" href="https://www.youtube.com/watch?v=cFTFtuEQ-10"> + Fast-paced overview of error handling fundamentals. Great for a quick refresher or introduction to the topic. + </Card> + <Card title="JavaScript Error Handling — The Coding Train" icon="video" href="https://www.youtube.com/watch?v=1Rq_LrpcgIM"> + Beginner-friendly explanation with live coding examples showing exactly when errors occur and how to handle them. + </Card> +</CardGroup> diff --git a/docs/concepts/es-modules.mdx b/docs/concepts/es-modules.mdx new file mode 100644 index 00000000..f6d1cd63 --- /dev/null +++ b/docs/concepts/es-modules.mdx @@ -0,0 +1,1329 @@ +--- +title: "ES Modules: JavaScript's Native Module System" +sidebarTitle: "ES Modules: Native Module System" +description: "Learn ES Modules in JavaScript. Understand import/export syntax, why ESM beats CommonJS, live bindings, dynamic imports, top-level await, and how modules enable tree-shaking." +--- + +Why does Node.js have two different module systems? Why can bundlers remove unused code from ES Modules but not from CommonJS? And why do some imports need curly braces while others don't? + +ES Modules (ESM) is JavaScript's official module system, standardized in ES2015. It's the answer to years of competing module formats, and it's designed from the ground up to be statically analyzable, which unlocks optimizations that older systems simply can't match. + +```javascript +// math.js - Exporting functionality +export const PI = 3.14159 +export function square(x) { + return x * x +} + +// app.js - Importing what you need +import { PI, square } from './math.js' + +console.log(square(4)) // 16 +console.log(PI) // 3.14159 +``` + +This guide goes beyond the basics. You'll learn why ESM's design makes it better than CommonJS for tooling and optimization, how live bindings work, and the practical differences between browsers and Node.js. + +<Info> +**What you'll learn in this guide:** +- Why ES Modules exist and what problems they solve +- The key differences between ESM and CommonJS (and when each applies) +- How live bindings make ESM exports work differently than CommonJS +- All the export and import syntax variations +- Dynamic imports for code splitting and lazy loading +- Top-level await and when to use it +- Browser vs Node.js: how ESM works in each environment +- Import maps for bare module specifiers in browsers +- How ESM enables tree-shaking and smaller bundles +</Info> + +<Warning> +**Prerequisite:** This guide assumes you're familiar with basic module concepts. If terms like "named exports" or "default exports" are new to you, start with our [IIFE, Modules & Namespaces](/concepts/iife-modules) guide first. +</Warning> + +--- + +## Why ES Modules Matter + +For most of JavaScript's history, there was no built-in way to split code into reusable pieces. The language simply didn't have modules. Developers created workarounds: IIFEs to avoid polluting the global scope, the Module Pattern for encapsulation, and eventually third-party systems like CommonJS (for Node.js) and AMD (for browsers). + +These solutions worked, but they were all invented outside the language itself. Each had tradeoffs, and none could be fully optimized by JavaScript engines or build tools. + +ES Modules changed that. Introduced in ES2015 (ES6), ESM is part of the language specification. This means: + +- **Browsers can load modules natively** without bundlers (though bundlers still help with optimization) +- **Tools can analyze your code statically** because imports and exports are declarative +- **Unused code can be eliminated** (tree-shaking) because the module graph is known at build time +- **The syntax is standardized** across all JavaScript environments + +Today, ESM is supported in all modern browsers and Node.js. It's the module system you should use for new projects. + +--- + +## The Shipping Container Analogy + +Think of ES Modules like the standardized shipping container that revolutionized global trade. + +Before shipping containers, cargo was loaded piece by piece. Every ship, truck, and warehouse had different ways of handling goods. It was slow, error-prone, and impossible to optimize at scale. + +Shipping containers changed everything. A standard size meant cranes, ships, and trucks could all handle cargo the same way. You could plan logistics before the ship even arrived because you knew exactly what you were dealing with. + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ COMMONJS vs ES MODULES │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ COMMONJS (Dynamic Loading) ES MODULES (Static Analysis) │ +│ ─────────────────────────── ──────────────────────────── │ +│ │ +│ ┌──────────────────────┐ ┌──────────────────────┐ │ +│ │ require('./math') │ │ import { add } │ │ +│ │ │ │ from './math.js' │ │ +│ │ Resolved at │ │ │ │ +│ │ RUNTIME │ │ Known at │ │ +│ │ │ │ BUILD TIME │ │ +│ │ Could be anything: │ │ │ │ +│ │ require(userInput) │ │ Tools can: │ │ +│ │ require(condition │ │ • See all imports │ │ +│ │ ? 'a' : 'b') │ │ • Remove dead code │ │ +│ │ │ │ • Optimize bundles │ │ +│ └──────────────────────┘ └──────────────────────┘ │ +│ │ +│ Like loose cargo: Like shipping containers: │ +│ flexible but hard standardized and │ +│ to optimize optimizable │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +ESM's static structure is like those shipping containers. Because imports and exports are declarative (not computed at runtime), tools can "see" your entire module graph before running any code. This visibility enables optimizations that are simply impossible with dynamic systems like CommonJS. + +--- + +## ESM vs CommonJS: The Complete Comparison + +If you've worked with Node.js, you've used CommonJS. It's been Node's module system since the beginning. But ESM and CommonJS work differently at a core level. + +| Aspect | ES Modules | CommonJS | +|--------|------------|----------| +| **Syntax** | `import` / `export` | `require()` / `module.exports` | +| **Loading** | Asynchronous | Synchronous | +| **Analysis** | Static (build time) | Dynamic (runtime) | +| **Exports** | Live bindings (references) | Value copies | +| **Strict mode** | Always enabled | Optional | +| **Top-level `this`** | `undefined` | `module.exports` | +| **File extensions** | Required in browsers | Optional in Node | +| **Tree-shaking** | Yes | No | + +### Syntax Side-by-Side + +```javascript +// ───────────────────────────────────────────── +// COMMONJS (Node.js traditional) +// ───────────────────────────────────────────── + +// Exporting +const PI = 3.14159 +function square(x) { return x * x } + +module.exports = { PI, square } +// or: exports.PI = PI + +// Importing +const { PI, square } = require('./math') +const math = require('./math') // whole module + + +// ───────────────────────────────────────────── +// ES MODULES (modern standard) +// ───────────────────────────────────────────── + +// Exporting +export const PI = 3.14159 +export function square(x) { return x * x } + +// Importing +import { PI, square } from './math.js' +import * as math from './math.js' // namespace import +``` + +### Static vs Dynamic: Why It Matters + +CommonJS imports are function calls that happen at runtime. You can put them anywhere, compute the path dynamically, and even conditionally require different modules: + +```javascript +// CommonJS - Dynamic (works but prevents optimization) +const moduleName = condition ? 'moduleA' : 'moduleB' +const mod = require(`./${moduleName}`) + +if (needsFeature) { + const feature = require('./heavy-feature') +} +``` + +ESM imports must be at the top level with string literals. This seems restrictive, but it's a feature, not a bug: + +```javascript +// ES Modules - Static (enables optimization) +import { feature } from './heavy-feature.js' // must be top-level +import { helper } from './utils.js' // path must be a string + +// ❌ These are syntax errors in ESM: +// import { x } from condition ? 'a.js' : 'b.js' +// if (condition) { import { y } from './module.js' } +``` + +Because ESM imports are static, bundlers can build a complete picture of your dependencies before running any code. This enables dead code elimination, bundle splitting, and other optimizations. + +<Tip> +**Need dynamic loading in ESM?** Use `import()` for dynamic imports (covered later in this guide). You get the best of both worlds: static analysis for your main code, dynamic loading when you actually need it. +</Tip> + +### Async vs Sync Loading + +CommonJS loads modules synchronously. When Node.js hits a `require()`, it blocks until the file is read and executed. This works fine on a server with fast disk access. + +ESM loads modules asynchronously. The browser fetches module files over the network, which can't block the main thread. This async nature is why: + +- ESM works natively in browsers +- Top-level `await` is possible in ESM +- The loading behavior is more predictable + +--- + +## Live Bindings: Why ESM Exports Are Different + +Here's a difference that trips people up. When you import from a CommonJS module, you get a **copy** of the exported value. When you import from an ES Module, you get a **live binding**: a reference to the original variable. + +```javascript +// ───────────────────────────────────────────── +// counter.cjs (CommonJS) +// ───────────────────────────────────────────── +let count = 0 +function increment() { count++ } +function getCount() { return count } + +module.exports = { count, increment, getCount } + + +// ───────────────────────────────────────────── +// main.cjs (CommonJS consumer) +// ───────────────────────────────────────────── +const { count, increment, getCount } = require('./counter.cjs') + +console.log(count) // 0 +increment() +console.log(count) // 0 (still! it's a copy) +console.log(getCount()) // 1 (function reads the real value) +``` + +```javascript +// ───────────────────────────────────────────── +// counter.mjs (ES Module) +// ───────────────────────────────────────────── +export let count = 0 +export function increment() { count++ } + + +// ───────────────────────────────────────────── +// main.mjs (ESM consumer) +// ───────────────────────────────────────────── +import { count, increment } from './counter.mjs' + +console.log(count) // 0 +increment() +console.log(count) // 1 (live binding reflects the change!) +``` + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ LIVE BINDINGS EXPLAINED │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ COMMONJS (Value Copy) ES MODULES (Live Binding) │ +│ ───────────────────── ──────────────────────── │ +│ │ +│ counter.js: counter.js: │ +│ ┌─────────────┐ ┌─────────────┐ │ +│ │ count: 1 │ │ count: 1 │ ◄───────┐ │ +│ └─────────────┘ └─────────────┘ │ │ +│ │ ▲ │ │ +│ │ copy at │ reference │ │ +│ │ require time │ always │ │ +│ ▼ │ current │ │ +│ main.js: main.js: │ │ +│ ┌─────────────┐ ┌─────────────┐ │ │ +│ │ count: 0 │ (stale!) │ count ──────┼─────────┘ │ +│ └─────────────┘ └─────────────┘ │ +│ │ +│ The imported value is The import IS the │ +│ frozen at require time original variable │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### Why Live Bindings Matter + +Live bindings have practical implications: + +1. **Singleton state works correctly** — If a module exports state, all importers see the same state +2. **Circular dependencies are safer** — Because bindings are live, you can have modules that depend on each other (though you should still avoid this when possible) +3. **You can't reassign imports** — `count = 5` throws an error because you don't own that binding + +```javascript +import { count } from './counter.js' + +count = 10 // ❌ TypeError: imported bindings are read-only + // Even though 'count' is 'let' in the source, you can't reassign it here +``` + +<Note> +Imported bindings are always read-only to the importer. Only the module that exports a variable can modify it. This prevents confusing "action at a distance" bugs. +</Note> + +--- + +## Export Syntax Deep Dive + +ES Modules give you several ways to export functionality. Here's the complete picture. + +### Named Exports + +The most common pattern. You can export inline or group exports at the bottom: + +```javascript +// Inline named exports +export const PI = 3.14159 +export function calculateArea(radius) { + return PI * radius * radius +} +export class Circle { + constructor(radius) { + this.radius = radius + } +} + +// Or group them at the bottom (same result) +const PI = 3.14159 +function calculateArea(radius) { + return PI * radius * radius +} +class Circle { + constructor(radius) { + this.radius = radius + } +} + +export { PI, calculateArea, Circle } +``` + +### Renaming Exports + +Use `as` to export under a different name: + +```javascript +function internalHelper() { /* ... */ } + +export { internalHelper as helper } +// Consumers import as: import { helper } from './module.js' +``` + +### Default Exports + +Each module can have one default export. It represents the module's "main" thing: + +```javascript +// A class as default export +export default class Logger { + log(message) { + console.log(`[LOG] ${message}`) + } +} + +// Or a function +export default function formatDate(date) { + return date.toISOString() +} + +// Or a value (note: no variable declaration with default) +export default { + name: 'Config', + version: '1.0.0' +} +``` + +### Mixing Named and Default Exports + +You can have both, though use this sparingly: + +```javascript +// React does this: default for the main API, named for utilities +export default function React() { /* ... */ } +export function useState() { /* ... */ } +export function useEffect() { /* ... */ } + +// Consumer can import both: +import React, { useState, useEffect } from 'react' +``` + +### Re-Exporting (Barrel Files) + +Re-exports let you aggregate multiple modules into one entry point. This is common in libraries: + +```javascript +// utils/index.js (barrel file) +export { formatDate, parseDate } from './date.js' +export { formatCurrency } from './currency.js' +export { default as Logger } from './logger.js' + +// Re-export everything from a module +export * from './math.js' + +// Re-export with rename +export { helper as utilHelper } from './helpers.js' +``` + +Now consumers can import from one place: + +```javascript +import { formatDate, formatCurrency, Logger } from './utils/index.js' +``` + +<Warning> +**Barrel file gotcha:** Re-exporting everything with `export *` can hurt tree-shaking. The bundler may include code you don't use. Prefer explicit re-exports for better optimization. +</Warning> + +--- + +## Import Syntax Deep Dive + +Every export style has a corresponding import style. + +### Named Imports + +Import specific exports by name (must match exactly): + +```javascript +import { PI, calculateArea } from './math.js' +import { formatDate } from './date.js' +``` + +### Renaming Imports + +Use `as` when names conflict or you want something clearer: + +```javascript +import { formatDate as formatDateISO } from './date.js' +import { formatDate as formatDateUS } from './date-us.js' +``` + +### Default Imports + +No curly braces. You choose the name: + +```javascript +// The module exports: export default class Logger { } +import Logger from './logger.js' // common convention: match the export +import MyLogger from './logger.js' // but any name works +import L from './logger.js' // even short names +``` + +### Namespace Imports + +Import everything as a single object: + +```javascript +import * as math from './math.js' + +console.log(math.PI) // 3.14159 +console.log(math.calculateArea(5)) // 78.54 +console.log(math.default) // the default export, if any +``` + +### Combined Imports + +Mixing default and named in one statement: + +```javascript +// Module exports both default and named +import React, { useState, useEffect } from 'react' +import lodash, { debounce, throttle } from 'lodash' +``` + +### Side-Effect Imports + +Import a module just for its side effects (no bindings): + +```javascript +import './polyfills.js' // runs the file, imports nothing +import './analytics.js' // sets up tracking +import './styles.css' // with bundler support +``` + +### Module Specifiers + +The string after `from` is called the module specifier: + +```javascript +// Relative paths (start with ./ or ../) +import { x } from './utils.js' +import { y } from '../shared/helpers.js' + +// Absolute paths (less common) +import { z } from '/lib/utils.js' + +// Bare specifiers (no path prefix) +import { useState } from 'react' // needs bundler or import map +import lodash from 'lodash' +``` + +<Tip> +**Bare specifiers** like `'react'` don't work in browsers by default — browsers don't know where to find `'react'`. You need either a bundler or an import map (covered later). +</Tip> + +--- + +## Module Characteristics + +ES Modules have built-in behaviors that differ from regular scripts. + +### Automatic Strict Mode + +Every ES Module runs in strict mode automatically. No `"use strict"` needed: + +```javascript +// In a module, this throws an error: +undeclaredVariable = 'oops' // ReferenceError: undeclaredVariable is not defined + +// These also fail: +delete Object.prototype // TypeError +function f(a, a) {} // SyntaxError: duplicate parameter +``` + +### Module Scope + +Variables in a module are local to that module, not global: + +```javascript +// module.js +const privateValue = 'secret' // not on window/global +var alsoPrivate = 'hidden' // var doesn't leak to global either + +// Only exports are accessible from outside +export const publicValue = 'visible' +``` + +### Singleton Behavior + +A module's code runs exactly once, no matter how many times you import it: + +```javascript +// counter.js +console.log('Module initialized!') // logs once +export let count = 0 + +// a.js +import { count } from './counter.js' // "Module initialized!" + +// b.js +import { count } from './counter.js' // nothing logged (already ran) +``` + +This makes modules natural singletons. All importers share the same instance. + +### `this` is `undefined` + +At the top level of a module, `this` is `undefined` (not `window` or `global`): + +```javascript +// script.js (regular script) +console.log(this) // window (in browser) + +// module.js (ES Module) +console.log(this) // undefined +``` + +### Import Hoisting + +Imports are hoisted to the top of the module. You can reference imported values before the import statement in code order (though you shouldn't): + +```javascript +// This works (but don't write code like this) +console.log(helper()) // imports are hoisted + +import { helper } from './utils.js' +``` + +### Deferred Execution in Browsers + +Module scripts are deferred by default. They don't block HTML parsing and execute after the document is parsed: + +```html +<!-- Blocks parsing until loaded and executed --> +<script src="blocking.js"></script> + +<!-- Deferred automatically (like adding defer attribute) --> +<script type="module" src="module.js"></script> +``` + +--- + +## Dynamic Imports + +Static imports must be at the top level, but sometimes you need to load modules dynamically. That's what `import()` is for. + +### The `import()` Expression + +[`import()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import) looks like a function call, but it's special syntax. It returns a Promise that resolves to the module's namespace object: + +```javascript +// Load a module dynamically +const module = await import('./math.js') +console.log(module.PI) // 3.14159 +console.log(module.default) // the default export, if any + +// Or with .then() +import('./math.js').then(module => { + console.log(module.PI) +}) +``` + +### Accessing Exports + +With dynamic imports, you get a module namespace object: + +```javascript +// Named exports are properties +const { formatDate, parseDate } = await import('./date.js') + +// Default export is on the 'default' property +const { default: Logger } = await import('./logger.js') +// or +const loggerModule = await import('./logger.js') +const Logger = loggerModule.default +``` + +### Real-World Use Cases + +**Route-based code splitting:** + +```javascript +// Load page components only when navigating +async function loadPage(pageName) { + const pages = { + home: () => import('./pages/Home.js'), + about: () => import('./pages/About.js'), + contact: () => import('./pages/Contact.js') + } + + const pageModule = await pages[pageName]() + return pageModule.default +} +``` + +**Conditional feature loading:** + +```javascript +// Only load heavy charting library if user needs it +async function showChart(data) { + const { Chart } = await import('chart.js') + const chart = new Chart(canvas, { /* ... */ }) +} +``` + +**Lazy loading based on feature detection:** + +```javascript +let crypto + +if (typeof window !== 'undefined' && window.crypto) { + crypto = window.crypto +} else { + // Only load polyfill in environments that need it + const module = await import('crypto-polyfill') + crypto = module.default +} +``` + +**Loading based on user preference:** + +```javascript +async function loadTheme(themeName) { + // Path is computed at runtime - not possible with static imports + const theme = await import(`./themes/${themeName}.js`) + applyTheme(theme.default) +} +``` + +<Note> +`import()` works in regular scripts too, not just modules. This is useful for adding ESM libraries to legacy codebases. +</Note> + +--- + +## Top-Level Await + +ES Modules support `await` at the top level, outside of any function. This is useful for setup that requires async operations. + +```javascript +// config.js +const response = await fetch('/api/config') +export const config = await response.json() + +// database.js +import { MongoClient } from 'mongodb' +const client = new MongoClient(uri) +await client.connect() +export const db = client.db('myapp') +``` + +### How It Affects Module Loading + +When a module uses top-level await, it blocks modules that depend on it: + +```javascript +// slow.js +await new Promise(r => setTimeout(r, 2000)) // 2 second delay +export const value = 42 + +// app.js +import { value } from './slow.js' // waits for slow.js to finish +console.log(value) // logs after 2 seconds +``` + +Modules that don't depend on `slow.js` can still load in parallel. + +### When to Use (and When Not To) + +**Good uses:** + +```javascript +// Loading configuration at startup +export const config = await loadConfig() + +// Database connection that's needed before anything else +export const db = await connectToDatabase() + +// One-time initialization +await initializeAnalytics() +``` + +**Avoid:** + +```javascript +// ❌ Don't do slow operations that could be lazy +const heavyData = await fetch('/api/huge-dataset') // blocks everything + +// ✓ Better: export a function that fetches when needed +export async function getHeavyData() { + return fetch('/api/huge-dataset') +} +``` + +<Warning> +Top-level await can create waterfall loading. If module A awaits and module B depends on A, then module C depends on B, everything loads sequentially. Use it judiciously. +</Warning> + +--- + +## Browser vs Node.js: ESM Differences + +ES Modules work in both browsers and Node.js, but there are differences in how you enable and use them. + +### Enabling ESM + +| Environment | How to Enable | +|-------------|---------------| +| **Browser** | `<script type="module" src="app.js"></script>` | +| **Node.js** | Use `.mjs` extension, or set `"type": "module"` in package.json | + +**Browser:** + +```html +<!-- The type="module" attribute enables ESM --> +<script type="module" src="./app.js"></script> + +<!-- Inline module --> +<script type="module"> + import { greet } from './utils.js' + greet('World') +</script> +``` + +**Node.js:** + +```javascript +// Option 1: Use .mjs extension +// math.mjs +export const add = (a, b) => a + b + +// Option 2: Set type in package.json +// package.json: { "type": "module" } +// Then .js files are treated as ESM +``` + +### File Extensions + +| Environment | Extension Required? | +|-------------|---------------------| +| **Browser** | Yes — must include `.js` or full URL | +| **Node.js** | Yes for ESM (can omit for CommonJS) | + +```javascript +// Browser - extensions required +import { helper } from './utils.js' // ✓ +import { helper } from './utils' // ❌ 404 error + +// Node.js ESM - extensions required +import { helper } from './utils.js' // ✓ +import { helper } from './utils' // ❌ ERR_MODULE_NOT_FOUND +``` + +### Bare Specifiers + +```javascript +import lodash from 'lodash' // "bare specifier" - no path prefix +``` + +| Environment | Bare Specifier Support | +|-------------|------------------------| +| **Browser** | No (needs import map or bundler) | +| **Node.js** | Yes (looks in node_modules) | + +### `import.meta` + +Both environments provide [`import.meta`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import.meta), but with different properties: + +```javascript +// Browser +console.log(import.meta.url) // "https://example.com/js/app.js" + +// Node.js +console.log(import.meta.url) // "file:///path/to/app.js" +console.log(import.meta.dirname) // "/path/to" (Node v20.11.0+) +console.log(import.meta.filename) // "/path/to/app.js" (Node v20.11.0+) +``` + +### CORS in Browsers + +When loading modules from different origins, browsers enforce CORS: + +```html +<!-- Same-origin: works fine --> +<script type="module" src="/js/app.js"></script> + +<!-- Cross-origin: server must send CORS headers --> +<script type="module" src="https://other-site.com/module.js"></script> +<!-- Requires: Access-Control-Allow-Origin header --> +``` + +### Summary Table + +| Feature | Browser | Node.js | +|---------|---------|---------| +| Enable via | `type="module"` | `.mjs` or `"type": "module"` | +| File extensions | Required | Required for ESM | +| Bare specifiers | Import map needed | Works (node_modules) | +| Top-level await | Yes | Yes | +| `import.meta.url` | Full URL | `file://` path | +| CORS | Enforced | N/A | +| Runs in strict mode | Yes | Yes | + +--- + +## Import Maps + +[Import maps](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap) solve a browser problem: how do you use bare specifiers like `'lodash'` without a bundler? + +### The Problem + +This works in Node.js because Node looks in `node_modules`: + +```javascript +import confetti from 'canvas-confetti' // Node: finds it in node_modules +``` + +In browsers, this fails — the browser doesn't know where `'canvas-confetti'` lives. + +### The Solution: Import Maps + +An import map tells the browser where to find modules: + +```html +<script type="importmap"> +{ + "imports": { + "canvas-confetti": "https://cdn.jsdelivr.net/npm/canvas-confetti@1.6.0/dist/confetti.module.mjs", + "lodash": "https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/lodash.js" + } +} +</script> + +<script type="module"> + // Now bare specifiers work! + import confetti from 'canvas-confetti' + import { debounce } from 'lodash' + + confetti() +</script> +``` + +### Path Prefixes + +Map entire package paths: + +```html +<script type="importmap"> +{ + "imports": { + "lodash/": "https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/" + } +} +</script> + +<script type="module"> + // The trailing slash enables path mapping + import debounce from 'lodash/debounce.js' + import throttle from 'lodash/throttle.js' +</script> +``` + +### Browser Support + +Import maps are supported in all modern browsers (Chrome 89+, Safari 16.4+, Firefox 108+). For older browsers, you'll need a polyfill or bundler. + +<Tip> +Import maps are great for simple projects, demos, and learning. For production apps with many dependencies, bundlers like Vite still provide better optimization and developer experience. +</Tip> + +--- + +## Tree-Shaking and Bundlers + +One of ESM's biggest advantages is enabling tree-shaking, which bundlers use to eliminate dead code. + +### What is Tree-Shaking? + +Tree-shaking removes unused exports from your final bundle: + +```javascript +// 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 } + +// app.js +import { add } from './math.js' +console.log(add(2, 3)) +``` + +A tree-shaking bundler sees that only `add` is used, so `subtract`, `multiply`, and `divide` are removed from the bundle. + +### Why ESM Enables This + +CommonJS can't be reliably tree-shaken because imports are dynamic: + +```javascript +// CommonJS - bundler can't know which exports are used +const math = require('./math') +const operation = userInput === 'add' ? math.add : math.subtract +``` + +ESM imports are static declarations, so the bundler knows exactly what's imported: + +```javascript +// ESM - bundler knows only 'add' is used +import { add } from './math.js' +``` + +### Modern Bundlers + +Even with native ESM support in browsers, bundlers remain valuable for: + +- **Tree-shaking** — Remove unused code +- **Code splitting** — Break your app into smaller chunks +- **Minification** — Shrink code for production +- **Transpilation** — Support older browsers +- **Asset handling** — Import CSS, images, JSON + +Popular options: +- **Vite** — Fast development, Rollup-based production builds +- **esbuild** — Extremely fast, great for libraries +- **Rollup** — Best tree-shaking, ideal for libraries +- **Webpack** — Most features, larger projects + +<Note> +For small projects or learning, you can use native ESM in browsers without a bundler. For production apps, bundlers still provide significant benefits. +</Note> + +--- + +## Common Mistakes + +### Mistake #1: Named vs Default Import Confusion + +This is the most common ESM mistake. The syntax looks similar but means different things: + +```javascript +// ───────────────────────────────────────────── +// The module exports this: +export default function Logger() {} +export function format() {} + +// ───────────────────────────────────────────── + +// ❌ WRONG - trying to import default as named +import { Logger } from './logger.js' +// Error: The module doesn't have a named export called 'Logger' + +// ✓ CORRECT - no braces for default +import Logger from './logger.js' + +// ✓ CORRECT - braces for named exports +import { format } from './logger.js' + +// ✓ CORRECT - both together +import Logger, { format } from './logger.js' +``` + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ THE CURLY BRACE RULE │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ export default X → import X from '...' (no braces) │ +│ export { Y } → import { Y } from '...' (braces) │ +│ export { Z as W } → import { W } from '...' (braces) │ +│ │ +│ Default = main thing, you name it │ +│ Named = specific items, names must match │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### Mistake #2: Circular Dependencies + +When module A imports module B, and module B imports module A, you can get `undefined` values: + +```javascript +// a.js +import { b } from './b.js' +export const a = 'A' +console.log('In a.js, b is:', b) + +// b.js +import { a } from './a.js' +export const b = 'B' +console.log('In b.js, a is:', a) + +// Running a.js throws: +// ReferenceError: Cannot access 'a' before initialization +// (a.js hasn't finished executing when b.js tries to access 'a') +``` + +**Fix:** Restructure to avoid circular deps, or use functions that defer access until runtime: + +```javascript +// Better: export functions that read values at call time +export function getA() { return a } +``` + +### Mistake #3: Missing File Extensions in Browsers + +```javascript +// ❌ WRONG in browsers +import { helper } from './utils' // 404 error + +// ✓ CORRECT +import { helper } from './utils.js' +``` + +### Mistake #4: Mixing CommonJS and ESM in Node.js + +You can't use `require()` in an ESM file or `import` in a CommonJS file without extra steps: + +```javascript +// ❌ In an ESM file (.mjs or type: module) +const fs = require('fs') // ReferenceError: require is not defined + +// ✓ CORRECT in ESM +import fs from 'fs' +import { readFile } from 'fs/promises' + +// ✓ If you really need require in ESM +import { createRequire } from 'module' +const require = createRequire(import.meta.url) +const legacyModule = require('some-commonjs-package') +``` + +--- + +## Key Takeaways + +<Info> +**The key things to remember:** + +1. **ESM is JavaScript's official module system** — It's standardized, works in browsers natively, and is the future of JavaScript modules. + +2. **Static structure enables optimization** — Because imports are declarations, not function calls, tools can analyze your code and remove unused exports (tree-shaking). + +3. **Live bindings, not copies** — ESM exports are references to the original variable. Changes in the source module are reflected in importers. CommonJS exports are value copies. + +4. **Use curly braces for named imports, no braces for default** — `import { named }` vs `import defaultExport`. Mixing these up is the #1 beginner mistake. + +5. **Dynamic imports for code splitting** — Use `import()` when you need to load modules conditionally or lazily. It returns a Promise. + +6. **ESM is always strict mode** — No need for `"use strict"`. Variables don't leak to global scope. + +7. **Modules execute once** — No matter how many files import a module, its top-level code runs exactly once. Modules are singletons. + +8. **File extensions are required** — In browsers and Node.js ESM, you must include `.js`. No automatic extension resolution. + +9. **Import maps solve bare specifiers in browsers** — Without a bundler, use import maps to tell browsers where to find packages like `'lodash'`. + +10. **Bundlers still matter** — Even with native ESM support, bundlers provide tree-shaking, minification, and code splitting that improve production performance. +</Info> + +--- + +## Test Your Knowledge + +<AccordionGroup> + <Accordion title="What's the fundamental difference between ESM and CommonJS that enables tree-shaking?"> + **Answer:** + + ESM imports are **static** — they must be at the top level with string literals. This means bundlers can analyze the entire dependency graph at build time without running any code. + + CommonJS uses **dynamic** `require()` calls that execute at runtime. The module path can be computed (`require(variable)`), used conditionally, or placed anywhere in code. Bundlers can't know what's actually imported until the code runs. + + ```javascript + // ESM - bundler sees exactly what's imported + import { add } from './math.js' // static, analyzable + + // CommonJS - bundler can't be certain + const op = condition ? 'add' : 'subtract' + const math = require('./math') + math[op](1, 2) // which function is used? Unknown until runtime + ``` + </Accordion> + + <Accordion title="What are 'live bindings' and how do they differ from CommonJS exports?"> + **Answer:** + + In ESM, imported bindings are **live references** to the exported variables. If the source module changes the value, importers see the new value. + + In CommonJS, `module.exports` provides **value copies** at the time of `require()`. Later changes in the source don't affect what was imported. + + ```javascript + // ESM: live binding + // counter.mjs + export let count = 0 + export function increment() { count++ } + + // main.mjs + import { count, increment } from './counter.mjs' + console.log(count) // 0 + increment() + console.log(count) // 1 (live!) + + // CommonJS: copy + // counter.cjs + let count = 0 + module.exports = { count, increment: () => count++ } + + // main.cjs + const { count, increment } = require('./counter.cjs') + console.log(count) // 0 + increment() + console.log(count) // 0 (still - it's a copy) + ``` + </Accordion> + + <Accordion title="When would you use dynamic imports over static imports?"> + **Answer:** + + Use `import()` when you need to: + + 1. **Load modules conditionally** — Based on user action, feature flags, or environment + 2. **Code split** — Load heavy components only when needed (route-based splitting) + 3. **Compute the module path** — The path is determined at runtime + 4. **Load modules in non-module scripts** — `import()` works even in regular scripts + + ```javascript + // Route-based code splitting + async function loadPage(route) { + const page = await import(`./pages/${route}.js`) + return page.default + } + + // Conditional loading + if (userWantsCharts) { + const { Chart } = await import('chart.js') + } + ``` + + Static imports are better when you always need the module — they're faster to analyze and optimize. + </Accordion> + + <Accordion title="Why do browsers require file extensions in imports, but Node.js CommonJS doesn't?"> + **Answer:** + + **Browsers** make HTTP requests for imports. Without an extension, the browser doesn't know what URL to request. It can't try multiple extensions (`.js`, `.mjs`, `/index.js`) because each would be a separate network request. + + **Node.js CommonJS** runs on the local file system where checking multiple file variations is fast. It tries: exact path → `.js` → `.json` → `.node` → `/index.js`, etc. + + **Node.js ESM** chose to require extensions for consistency with browsers and to avoid the ambiguity of the CommonJS resolution algorithm. + + ```javascript + // Browser - must include extension + import { x } from './utils.js' // ✓ + import { x } from './utils' // ❌ 404 + + // Node CommonJS - extension optional + const x = require('./utils') // ✓ finds utils.js + + // Node ESM - extension required + import { x } from './utils.js' // ✓ + import { x } from './utils' // ❌ ERR_MODULE_NOT_FOUND + ``` + </Accordion> + + <Accordion title="What is an import map and when would you use one?"> + **Answer:** + + An import map is a JSON object that tells browsers how to resolve bare module specifiers (like `'lodash'`). It maps package names to URLs. + + ```html + <script type="importmap"> + { + "imports": { + "lodash": "https://cdn.jsdelivr.net/npm/lodash-es/lodash.js", + "lodash/": "https://cdn.jsdelivr.net/npm/lodash-es/" + } + } + </script> + + <script type="module"> + import { debounce } from 'lodash' // works now! + </script> + ``` + + **Use import maps when:** + - Building simple apps without a bundler + - Creating demos or examples + - Learning/prototyping + - You want CDN-based dependencies + + For production apps with many dependencies, bundlers usually provide better optimization. + </Accordion> + + <Accordion title="What happens if two modules import each other (circular dependency)?"> + **Answer:** + + ESM handles circular dependencies, but you can get errors for values that haven't been initialized yet. + + ```javascript + // a.js + import { b } from './b.js' + export const a = 'A' + console.log(b) // 'B' (b.js already ran) + + // b.js + import { a } from './a.js' + export const b = 'B' + console.log(a) // ReferenceError! (a.js hasn't finished) + ``` + + When b.js runs, a.js is still in the middle of executing (it imported b.js), so accessing `a` throws a `ReferenceError: Cannot access 'a' before initialization` because `const` declarations have a temporal dead zone (TDZ). + + **Solutions:** + 1. Restructure to avoid circular dependencies + 2. Move shared code to a third module + 3. Use functions that access values later (not at module load time) + + ```javascript + // Works: function accesses 'a' when called, not when defined + export function getA() { return a } + ``` + </Accordion> +</AccordionGroup> + +--- + +## Related Concepts + +<CardGroup cols={2}> + <Card title="IIFE, Modules & Namespaces" icon="box" href="/concepts/iife-modules"> + The history of JavaScript modules and foundational patterns + </Card> + <Card title="async/await" icon="clock" href="/concepts/async-await"> + Used with dynamic imports and top-level await + </Card> + <Card title="Scope and Closures" icon="lock" href="/concepts/scope-and-closures"> + How module scope isolates variables + </Card> + <Card title="Design Patterns" icon="shapes" href="/concepts/design-patterns"> + Module pattern and other encapsulation patterns + </Card> +</CardGroup> + +--- + +## Reference + +<CardGroup cols={2}> + <Card title="JavaScript Modules Guide — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules"> + Comprehensive guide to using modules in JavaScript + </Card> + <Card title="import statement — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import"> + Complete reference for static import syntax + </Card> + <Card title="export statement — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export"> + Complete reference for export syntax + </Card> + <Card title="import() operator — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import"> + Dynamic import syntax and behavior + </Card> + <Card title="import.meta — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import.meta"> + Module metadata including URL and Node.js properties + </Card> + <Card title="Import maps — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap"> + Browser support for bare module specifiers + </Card> +</CardGroup> + +--- + +## Articles + +<CardGroup cols={2}> + <Card title="ES Modules: A Cartoon Deep-Dive" icon="newspaper" href="https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/"> + Lin Clark's illustrated guide explains how ES Modules work under the hood. The best visual explanation of module loading, linking, and evaluation you'll find. + </Card> + <Card title="JavaScript Modules" icon="newspaper" href="https://javascript.info/modules"> + The javascript.info series covers modules comprehensively. Includes interactive examples and exercises to test your understanding. + </Card> + <Card title="Node.js ES Modules Documentation" icon="newspaper" href="https://nodejs.org/api/esm.html"> + The official Node.js documentation for ES Modules. Covers enabling ESM, interoperability with CommonJS, import.meta, and the resolution algorithm. + </Card> + <Card title="ES6 Modules in Depth" icon="newspaper" href="https://ponyfoo.com/articles/es6-modules-in-depth"> + Nicolás Bevacqua's deep dive into module syntax and semantics. Great for understanding the design decisions behind ESM. + </Card> +</CardGroup> + +--- + +## Videos + +<CardGroup cols={2}> + <Card title="JavaScript ES6 Modules" icon="video" href="https://www.youtube.com/watch?v=cRHQNNcYf6s"> + Web Dev Simplified breaks down import/export syntax with clear examples. Perfect for solidifying your understanding of the basics. + </Card> + <Card title="ES Modules in 100 Seconds" icon="video" href="https://www.youtube.com/watch?v=qgRUr-YUk1Q"> + Fireship's rapid-fire overview of ES Modules. Great for a quick refresher or introduction to the key concepts. + </Card> + <Card title="JavaScript Modules Past & Present" icon="video" href="https://www.youtube.com/watch?v=GQ96b_u7rGc"> + Historical context on how JavaScript modules evolved from IIFEs to CommonJS to ESM. Helps you understand why ESM is designed the way it is. + </Card> +</CardGroup> diff --git a/docs/concepts/event-loop.mdx b/docs/concepts/event-loop.mdx new file mode 100644 index 00000000..731c4577 --- /dev/null +++ b/docs/concepts/event-loop.mdx @@ -0,0 +1,1685 @@ +--- +title: "Event Loop: How Async Code Actually Runs in JavaScript" +sidebarTitle: "Event Loop: How Async Code Actually Runs" +description: "Learn how the JavaScript event loop handles async code. Understand the call stack, task queue, microtasks, and why Promises always run before setTimeout()." +--- + +How does JavaScript handle multiple things at once when it can only do one thing at a time? Why does this code print in a surprising order? + +```javascript +console.log('Start'); +setTimeout(() => console.log('Timeout'), 0); +Promise.resolve().then(() => console.log('Promise')); +console.log('End'); + +// Output: +// Start +// End +// Promise +// Timeout +``` + +Even with a 0ms delay, `Timeout` prints last. The answer lies in the **[event loop](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Execution_model)**. It's JavaScript's mechanism for handling asynchronous operations while remaining single-threaded. + +<Info> +**What you'll learn in this guide:** +- Why JavaScript needs an event loop (and what "single-threaded" really means) +- How setTimeout REALLY works (spoiler: the delay is NOT guaranteed!) +- The difference between tasks and microtasks (and why it matters) +- Why `Promise.then()` runs before `setTimeout(..., 0)` +- How to use setTimeout, setInterval, and requestAnimationFrame effectively +- Common interview questions explained step-by-step +</Info> + +<Warning> +**Prerequisites:** This guide assumes familiarity with [the call stack](/concepts/call-stack) and [Promises](/concepts/promises). If those concepts are new to you, read them first! +</Warning> + +--- + +## What is the Event Loop? + +The **event loop** is JavaScript's mechanism for executing code, handling events, and managing asynchronous operations. It coordinates execution by checking callback queues when the call stack is empty, then pushing queued tasks to the stack for execution. This enables non-blocking behavior despite JavaScript being single-threaded. + +### The Restaurant Analogy + +Imagine a busy restaurant kitchen with a **single chef** who can only cook one dish at a time. Despite this limitation, the restaurant serves hundreds of customers because the kitchen has a clever system: + +``` +THE JAVASCRIPT KITCHEN + + ┌─────────────────────────┐ +┌────────────────────────────────┐ │ KITCHEN TIMERS │ +│ ORDER SPIKE │ │ (Web APIs) │ +│ (Call Stack) │ │ │ +│ ┌──────────────────────────┐ │ │ [Timer: 3 min - soup] │ +│ │ Currently cooking: │ │ │ [Timer: 10 min - roast]│ +│ │ "grilled cheese" │ │ │ [Waiting: delivery] │ +│ ├──────────────────────────┤ │ │ │ +│ │ Next: "prep salad" │ │ └───────────┬─────────────┘ +│ └──────────────────────────┘ │ │ +└────────────────────────────────┘ │ (timer done!) + ▲ ▼ + │ ┌──────────────────────────────┐ + │ │ "ORDER UP!" WINDOW │ + KITCHEN MANAGER │ (Task Queue) │ + (Event Loop) │ │ + │ [soup ready] [delivery here]│ + "Chef free? ────────────────────►│ │ + Here's the next order!" └──────────────────────────────┘ + │ ▲ + │ ┌───────────────┴──────────────┐ + │ │ VIP RUSH ORDERS │ + └──────────────────────────│ (Microtask Queue) │ + (VIP orders first!) │ │ + │ [plating] [garnish] │ + └──────────────────────────────┘ +``` + +Here's how it maps to JavaScript: + +| Kitchen | JavaScript | +|---------|------------| +| **Single Chef** | JavaScript engine (single-threaded) | +| **Order Spike** | Call Stack (current work, LIFO) | +| **Kitchen Timers** | Web APIs (setTimeout, fetch, etc.) | +| **"Order Up!" Window** | Task Queue (callbacks waiting) | +| **VIP Rush Orders** | Microtask Queue (promises, high priority) | +| **Kitchen Manager** | Event Loop (coordinator) | + +The chef (JavaScript) can only work on one dish (task) at a time. But kitchen timers (Web APIs) run independently! When a timer goes off, the dish goes to the "Order Up!" window (Task Queue). The kitchen manager (Event Loop) constantly checks: "Is the chef free? Here's the next order!" + +**VIP orders (Promises)** always get priority. They jump ahead of regular orders in the queue. + +<Note> +**TL;DR:** JavaScript is single-threaded but achieves concurrency by delegating work to browser APIs, which run in the background. When they're done, callbacks go into queues. The Event Loop moves callbacks from queues to the call stack when it's empty. +</Note> + +--- + +## The Problem: JavaScript is Single-Threaded + +JavaScript can only do **one thing at a time**. There's one call stack, one thread of execution. + +```javascript +// JavaScript executes these ONE AT A TIME, in order +console.log('First'); // 1. This runs +console.log('Second'); // 2. Then this +console.log('Third'); // 3. Then this +``` + +### Why Is This a Problem? + +Imagine if every operation blocked the entire program. Consider the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API): + +```javascript +// If fetch() was synchronous (blocking)... +const data = fetch('https://api.example.com/data'); // Takes 2 seconds +console.log(data); +// NOTHING else can happen for 2 seconds! +// - No clicking buttons +// - No scrolling +// - No animations +// - Complete UI freeze! +``` + +A 30-second API call would freeze your entire webpage for 30 seconds. Users would think the browser crashed! + +### The Solution: Asynchronous JavaScript + +JavaScript solves this by **delegating** long-running tasks to the browser (or Node.js), which handles them in the background. Functions like [`setTimeout()`](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout) don't block: + +```javascript +console.log('Start'); + +// This doesn't block! Browser handles the timer +setTimeout(() => { + console.log('Timer done'); +}, 2000); + +console.log('End'); + +// Output: +// Start +// End +// Timer done (after 2 seconds) +``` + +The secret sauce that makes this work? **The Event Loop**. + +--- + +## The JavaScript Runtime Environment + +To understand the Event Loop, you need to see the full picture: + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ JAVASCRIPT RUNTIME │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ JAVASCRIPT ENGINE (V8, SpiderMonkey, etc.) │ │ +│ │ ┌───────────────────────┐ ┌───────────────────────────┐ │ │ +│ │ │ CALL STACK │ │ HEAP │ │ │ +│ │ │ │ │ │ │ │ +│ │ │ ┌─────────────────┐ │ │ { objects stored here } │ │ │ +│ │ │ │ processData() │ │ │ [ arrays stored here ] │ │ │ +│ │ │ ├─────────────────┤ │ │ function references │ │ │ +│ │ │ │ fetchUser() │ │ │ │ │ │ +│ │ │ ├─────────────────┤ │ │ │ │ │ +│ │ │ │ main() │ │ │ │ │ │ +│ │ │ └─────────────────┘ │ └───────────────────────────┘ │ │ +│ │ └───────────────────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ BROWSER / NODE.js APIs │ │ +│ │ │ │ +│ │ setTimeout() setInterval() fetch() DOM events │ │ +│ │ requestAnimationFrame() IndexedDB WebSockets │ │ +│ │ │ │ +│ │ (These are handled outside of JavaScript execution!) │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ │ callbacks │ +│ ▼ │ +│ ┌──────────────────────────────────────────────────────────────────┐ │ +│ │ MICROTASK QUEUE TASK QUEUE (Macrotask) │ │ +│ │ ┌────────────────────────┐ ┌─────────────────────────┐ │ │ +│ │ │ Promise.then() │ │ setTimeout callback │ │ │ +│ │ │ queueMicrotask() │ │ setInterval callback │ │ │ +│ │ │ MutationObserver │ │ I/O callbacks │ │ │ +│ │ │ async/await (after) │ │ UI event handlers │ │ │ +│ │ └────────────────────────┘ │ Event handlers │ │ │ +│ │ ▲ └─────────────────────────┘ │ │ +│ │ │ HIGHER PRIORITY ▲ │ │ +│ └─────────┼────────────────────────────────────┼───────────────────┘ │ +│ │ │ │ +│ └──────────┬─────────────────────────┘ │ +│ │ │ +│ ┌────────┴────────┐ │ +│ │ EVENT LOOP │ │ +│ │ │ │ +│ │ "Is the call │ │ +│ │ stack empty?" ├──────────► Push next callback │ +│ │ │ to call stack │ +│ └─────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### The Components + +<AccordionGroup> + <Accordion title="Call Stack"> + The **[Call Stack](/concepts/call-stack)** is where JavaScript keeps track of what function is currently running. It's a LIFO (Last In, First Out) structure, like a stack of plates. + + ```javascript + function multiply(a, b) { + return a * b; + } + + function square(n) { + return multiply(n, n); + } + + function printSquare(n) { + const result = square(n); + console.log(result); + } + + printSquare(4); + ``` + + Call stack progression: + ``` + 1. [printSquare] + 2. [square, printSquare] + 3. [multiply, square, printSquare] + 4. [square, printSquare] // multiply returns + 5. [printSquare] // square returns + 6. [console.log, printSquare] + 7. [printSquare] // console.log returns + 8. [] // printSquare returns + ``` + </Accordion> + + <Accordion title="Heap"> + The **Heap** is a large, mostly unstructured region of memory where objects, arrays, and functions are stored. When you create an object, it lives in the heap. + + ```javascript + const user = { name: 'Alice' }; // Object stored in heap + const numbers = [1, 2, 3]; // Array stored in heap + ``` + </Accordion> + + <Accordion title="Web APIs (Browser) / C++ APIs (Node.js)"> + These are **NOT** part of JavaScript itself! They're provided by the environment: + + **Browser APIs:** + - [`setTimeout`](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout), [`setInterval`](https://developer.mozilla.org/en-US/docs/Web/API/setInterval) + - [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API), `XMLHttpRequest` + - DOM events (click, scroll, etc.) + - [`requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame) + - Geolocation, WebSockets, IndexedDB + + **Node.js APIs:** + - File system operations + - Network requests + - Timers + - Child processes + + These are handled by the browser/Node.js runtime outside of JavaScript execution, allowing JavaScript to remain non-blocking. + </Accordion> + + <Accordion title="Task Queue (Macrotask Queue)"> + The **Task Queue** holds callbacks from: + - `setTimeout` and `setInterval` + - I/O operations + - UI rendering tasks + - Event handlers (click, keypress, etc.) + - `setImmediate` (Node.js) + + Tasks are processed **one at a time**, with potential rendering between them. + </Accordion> + + <Accordion title="Microtask Queue"> + The **Microtask Queue** holds high-priority callbacks from: + - `Promise.then()`, `.catch()`, `.finally()` + - [`queueMicrotask()`](https://developer.mozilla.org/en-US/docs/Web/API/queueMicrotask) + - [`MutationObserver`](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) + - Code after `await` in [async functions](/concepts/async-await) + + **Microtasks ALWAYS run before the next task!** The entire microtask queue is drained before moving to the task queue. + </Accordion> + + <Accordion title="Event Loop"> + The **Event Loop** is the orchestrator. Its job is simple but crucial: + + ``` + FOREVER: + 1. Execute all code in the Call Stack until empty + 2. Execute ALL microtasks (until microtask queue is empty) + 3. Render if needed (update the UI) + 4. Take ONE task from the task queue + 5. Go to step 1 + ``` + + The key insight: **Microtasks can starve the task queue!** If microtasks keep adding more microtasks, tasks (and rendering) never get a chance to run. + </Accordion> +</AccordionGroup> + +--- + +## How the Event Loop Works: Step-by-Step + +Let's trace through some examples to see the event loop in action. + +### Example 1: Basic setTimeout + +```javascript +console.log('Start'); + +setTimeout(() => { + console.log('Timeout'); +}, 0); + +console.log('End'); +``` + +**Output:** `Start`, `End`, `Timeout` + +**Why?** Let's trace it step by step: + +<Steps> + <Step title="Execute console.log('Start')"> + Call stack: `[console.log]` → prints "Start" → stack empty + + ``` + Call Stack: [console.log('Start')] + Web APIs: [] + Task Queue: [] + Output: "Start" + ``` + </Step> + + <Step title="Execute setTimeout()"> + `setTimeout` is called → registers timer with Web APIs → immediately returns + + ``` + Call Stack: [] + Web APIs: [Timer: 0ms → callback] + Task Queue: [] + ``` + + The timer is handled by the browser, NOT JavaScript! + </Step> + + <Step title="Timer completes (0ms)"> + Browser's timer finishes → callback moves to Task Queue + + ``` + Call Stack: [] + Web APIs: [] + Task Queue: [callback] + ``` + </Step> + + <Step title="Execute console.log('End')"> + But wait! We're still running the main script! + + ``` + Call Stack: [console.log('End')] + Task Queue: [callback] + Output: "Start", "End" + ``` + </Step> + + <Step title="Main script complete, Event Loop checks queues"> + Call stack is empty → Event Loop takes callback from Task Queue + + ``` + Call Stack: [callback] + Task Queue: [] + Output: "Start", "End", "Timeout" + ``` + </Step> +</Steps> + +<Warning> +**Key insight:** Even with a 0ms delay, `setTimeout` callback NEVER runs immediately. It must wait for: +1. The current script to finish +2. All microtasks to complete +3. Its turn in the task queue +</Warning> + +### Example 2: Promises vs setTimeout + +```javascript +console.log('1'); + +setTimeout(() => console.log('2'), 0); + +Promise.resolve().then(() => console.log('3')); + +console.log('4'); +``` + +**Output:** `1`, `4`, `3`, `2` + +**Why does `3` come before `2`?** + +<Steps> + <Step title="Synchronous code runs first"> + `console.log('1')` → prints "1" + + `setTimeout` → registers callback in Web APIs → callback goes to **Task Queue** + + `Promise.resolve().then()` → callback goes to **Microtask Queue** + + `console.log('4')` → prints "4" + + ``` + Output so far: "1", "4" + Microtask Queue: [Promise callback] + Task Queue: [setTimeout callback] + ``` + </Step> + + <Step title="Microtasks run before tasks"> + Call stack empty → Event Loop checks **Microtask Queue first** + + Promise callback runs → prints "3" + + ``` + Output so far: "1", "4", "3" + Microtask Queue: [] + Task Queue: [setTimeout callback] + ``` + </Step> + + <Step title="Task Queue processed"> + Microtask queue empty → Event Loop takes from Task Queue + + setTimeout callback runs → prints "2" + + ``` + Final output: "1", "4", "3", "2" + ``` + </Step> +</Steps> + +<Tip> +**The Golden Rule:** Microtasks (Promises) ALWAYS run before Macrotasks (setTimeout), regardless of which was scheduled first. +</Tip> + +### Example 3: Nested Microtasks + +```javascript +console.log('Start'); + +Promise.resolve() + .then(() => { + console.log('Promise 1'); + Promise.resolve().then(() => console.log('Promise 2')); + }); + +setTimeout(() => console.log('Timeout'), 0); + +console.log('End'); +``` + +**Output:** `Start`, `End`, `Promise 1`, `Promise 2`, `Timeout` + +Even though the second promise is created AFTER setTimeout was registered, it still runs first because the **entire microtask queue must be drained** before any task runs! + +--- + +## Tasks vs Microtasks: The Complete Picture + +### What Creates Tasks (Macrotasks)? + +| Source | Description | +|--------|-------------| +| `setTimeout(fn, delay)` | Runs `fn` after at least `delay` ms | +| `setInterval(fn, delay)` | Runs `fn` repeatedly every ~`delay` ms | +| I/O callbacks | Network responses, file reads | +| UI Events | click, scroll, keydown, mousemove | +| `setImmediate(fn)` | Node.js only, runs after I/O | +| [`MessageChannel`](https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel) | `postMessage` callbacks | + +<Note> +**What about requestAnimationFrame?** rAF is NOT a task. It runs during the rendering phase, after microtasks but before the browser paints. It's covered in detail in the [Timers section](#requestanimationframe-smooth-animations). +</Note> + +### What Creates Microtasks? + +| Source | Description | +|--------|-------------| +| `Promise.then/catch/finally` | When promise settles | +| `async/await` | Code after `await` | +| `queueMicrotask(fn)` | Explicitly queue a microtask | +| `MutationObserver` | When DOM changes | + +### The Event Loop Algorithm (Simplified) + +```javascript +// Pseudocode for the Event Loop (per HTML specification) +while (true) { + // 1. Process ONE task from the task queue (if available) + if (taskQueue.hasItems()) { + const task = taskQueue.dequeue(); + execute(task); + } + + // 2. Process ALL microtasks (until queue is empty) + while (microtaskQueue.hasItems()) { + const microtask = microtaskQueue.dequeue(); + execute(microtask); + // New microtasks added during execution are also processed! + } + + // 3. Render if needed (browser decides, typically ~60fps) + if (shouldRender()) { + // 3a. Run requestAnimationFrame callbacks + runAnimationFrameCallbacks(); + // 3b. Perform style calculation, layout, and paint + render(); + } + + // 4. Repeat (go back to step 1) +} +``` + +<Warning> +**Microtask Starvation:** If microtasks keep adding more microtasks, the task queue (and rendering!) will never get a chance to run: + +```javascript +// DON'T DO THIS - infinite microtask loop! +function forever() { + Promise.resolve().then(forever); +} +forever(); // Browser freezes! +``` +</Warning> + +--- + +## JavaScript Timers: setTimeout, setInterval, requestAnimationFrame + +Now that you understand the event loop, let's dive deep into JavaScript's timing functions. + +### setTimeout: One-Time Delayed Execution + +```javascript +// Syntax +const timerId = setTimeout(callback, delay, ...args); + +// Cancel before it runs +clearTimeout(timerId); +``` + +**Basic usage:** + +```javascript +// Run after 2 seconds +setTimeout(() => { + console.log('Hello after 2 seconds!'); +}, 2000); + +// Pass arguments to the callback +setTimeout((name, greeting) => { + console.log(`${greeting}, ${name}!`); +}, 1000, 'Alice', 'Hello'); +// Output after 1s: "Hello, Alice!" +``` + +**Canceling a timeout:** + +```javascript +const timerId = setTimeout(() => { + console.log('This will NOT run'); +}, 5000); + +// Cancel it before it fires +clearTimeout(timerId); +``` + +#### The "Zero Delay" Myth + +`setTimeout(fn, 0)` does NOT run immediately! + +```javascript +console.log('A'); +setTimeout(() => console.log('B'), 0); +console.log('C'); + +// Output: A, C, B (NOT A, B, C!) +``` + +Even with 0ms delay, the callback must wait for: +1. Current script to complete +2. All microtasks to drain +3. Its turn in the task queue + +#### The Minimum Delay (4ms Rule) + +After 5 nested timeouts, browsers enforce a minimum 4ms delay: + +```javascript +let start = Date.now(); +let times = []; + +setTimeout(function run() { + times.push(Date.now() - start); + if (times.length < 10) { + setTimeout(run, 0); + } else { + console.log(times); + } +}, 0); + +// Typical output (varies by browser/system): [1, 1, 1, 1, 4, 9, 14, 19, 24, 29] +// First 4-5 are fast, then 4ms minimum kicks in +``` + +<Warning> +**setTimeout delay is a MINIMUM, not a guarantee!** + +```javascript +const start = Date.now(); + +setTimeout(() => { + console.log(`Actual delay: ${Date.now() - start}ms`); +}, 100); + +// Heavy computation blocks the event loop +for (let i = 0; i < 1000000000; i++) {} + +// Output might be: "Actual delay: 2547ms" (NOT 100ms!) +``` + +If the call stack is busy, the timeout callback must wait. +</Warning> + +### setInterval: Repeated Execution + +```javascript +// Syntax +const intervalId = setInterval(callback, delay, ...args); + +// Stop the interval +clearInterval(intervalId); +``` + +**Basic usage:** + +```javascript +let count = 0; + +const intervalId = setInterval(() => { + count++; + console.log(`Count: ${count}`); + + if (count >= 5) { + clearInterval(intervalId); + console.log('Done!'); + } +}, 1000); + +// Output every second: Count: 1, Count: 2, ... Count: 5, Done! +``` + +#### The setInterval Drift Problem + +`setInterval` doesn't account for callback execution time: + +```javascript +// Problem: If callback takes 300ms, and interval is 1000ms, +// actual time between START of callbacks is 1000ms, +// but time between END of one and START of next is only 700ms + +setInterval(() => { + // This takes 300ms to execute + heavyComputation(); +}, 1000); +``` + +``` +Time: 0ms 1000ms 2000ms 3000ms + │ │ │ │ +setInterval│───────│────────│────────│ + │ 300ms │ 300ms │ 300ms │ + │callback│callback│callback│ + │ │ │ │ + +The 1000ms is between STARTS, not between END and START +``` + +#### Solution: Nested setTimeout + +For more precise timing, use nested `setTimeout`: + +```javascript +// Nested setTimeout guarantees delay BETWEEN executions +function preciseInterval(callback, delay) { + function tick() { + callback(); + setTimeout(tick, delay); // Schedule next AFTER current completes + } + setTimeout(tick, delay); +} + +// Now there's exactly `delay` ms between the END of one +// callback and the START of the next +``` + +``` +Time: 0ms 1300ms 2600ms 3900ms + │ │ │ │ +Nested │───────│────────│────────│ +setTimeout│ 300ms│ 300ms │ 300ms │ + │ + │ + │ + │ + │ 1000ms│ 1000ms │ 1000ms │ + │ delay │ delay │ delay │ +``` + +<Tip> +**When to use which:** +- **setInterval**: For simple UI updates that don't depend on previous execution +- **Nested setTimeout**: For sequential operations, API polling, or when timing precision matters +</Tip> + +### requestAnimationFrame: Smooth Animations + +`requestAnimationFrame` (rAF) is designed specifically for animations. It syncs with the browser's refresh rate (usually 60fps = ~16.67ms per frame). + +```javascript +// Syntax +const rafId = requestAnimationFrame(callback); + +// Cancel +cancelAnimationFrame(rafId); +``` + +**Basic animation loop:** + +```javascript +function animate(timestamp) { + // timestamp = time since page load in ms + + // Update animation state + element.style.left = (timestamp / 10) + 'px'; + + // Request next frame + requestAnimationFrame(animate); +} + +// Start the animation +requestAnimationFrame(animate); +``` + +#### Why requestAnimationFrame is Better for Animations + +| Feature | setTimeout/setInterval | requestAnimationFrame | +|---------|----------------------|----------------------| +| **Sync with display** | No | Yes (matches refresh rate) | +| **Battery efficient** | No | Yes (pauses in background tabs) | +| **Smooth animations** | Can be janky | Optimized by browser | +| **Timing accuracy** | Can drift | Consistent frame timing | +| **CPU usage** | Runs even if tab hidden | Pauses when tab hidden | + +**Example: Animating with rAF** + +```javascript +const box = document.getElementById('box'); +let position = 0; +let lastTime = null; + +function animate(currentTime) { + // Handle first frame (no previous time yet) + if (lastTime === null) { + lastTime = currentTime; + requestAnimationFrame(animate); + return; + } + + // Calculate time since last frame + const deltaTime = currentTime - lastTime; + lastTime = currentTime; + + // Move 100 pixels per second, regardless of frame rate + const speed = 100; // pixels per second + position += speed * (deltaTime / 1000); + + box.style.transform = `translateX(${position}px)`; + + // Stop at 500px + if (position < 500) { + requestAnimationFrame(animate); + } +} + +requestAnimationFrame(animate); +``` + +#### When requestAnimationFrame Runs + +``` +One Event Loop Iteration: +┌─────────────────────────────────────────────────────────────────┐ +│ 1. Run task from Task Queue │ +├─────────────────────────────────────────────────────────────────┤ +│ 2. Run ALL microtasks │ +├─────────────────────────────────────────────────────────────────┤ +│ 3. If time to render: │ +│ a. Run requestAnimationFrame callbacks ← HERE! │ +│ b. Render/paint the screen │ +├─────────────────────────────────────────────────────────────────┤ +│ 4. If idle time remains before next frame: │ +│ Run requestIdleCallback callbacks (non-essential work) │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### Timer Comparison Summary + +<Tabs> + <Tab title="setTimeout"> + **Use for:** One-time delayed execution + + ```javascript + // Delay a function call + setTimeout(() => { + showNotification('Saved!'); + }, 2000); + + // Debouncing + let timeoutId; + input.addEventListener('input', () => { + clearTimeout(timeoutId); + timeoutId = setTimeout(search, 300); + }); + ``` + + **Gotchas:** + - Delay is minimum, not guaranteed + - 4ms minimum after 5 nested calls + - Blocked by long-running synchronous code + </Tab> + + <Tab title="setInterval"> + **Use for:** Repeated execution at fixed intervals + + ```javascript + // Update clock every second + setInterval(() => { + clock.textContent = new Date().toLocaleTimeString(); + }, 1000); + + // Poll server for updates + const pollId = setInterval(async () => { + const data = await fetchUpdates(); + updateUI(data); + }, 5000); + ``` + + **Gotchas:** + - Can drift if callbacks take long + - Multiple calls can queue up + - ALWAYS store the ID and call `clearInterval` + - Consider nested setTimeout for precision + </Tab> + + <Tab title="requestAnimationFrame"> + **Use for:** Animations and visual updates + + ```javascript + // Smooth animation + function animate() { + updatePosition(); + draw(); + requestAnimationFrame(animate); + } + requestAnimationFrame(animate); + + // Smooth scroll + function smoothScroll(target) { + const current = window.scrollY; + const distance = target - current; + + if (Math.abs(distance) > 1) { + window.scrollTo(0, current + distance * 0.1); + requestAnimationFrame(() => smoothScroll(target)); + } + } + ``` + + **Benefits:** + - Synced with display refresh (60fps) + - Pauses in background tabs (saves battery) + - Browser-optimized + </Tab> +</Tabs> + +--- + +## Classic Interview Questions + +### Question 1: Basic Output Order + +```javascript +console.log('1'); +setTimeout(() => console.log('2'), 0); +Promise.resolve().then(() => console.log('3')); +console.log('4'); +``` + +<Accordion title="Answer"> +**Output:** `1`, `4`, `3`, `2` + +**Explanation:** +1. `console.log('1')` — synchronous, runs immediately → "1" +2. `setTimeout` — callback goes to **Task Queue** +3. `Promise.then` — callback goes to **Microtask Queue** +4. `console.log('4')` — synchronous, runs immediately → "4" +5. Call stack empty → drain Microtask Queue → "3" +6. Microtask queue empty → process Task Queue → "2" +</Accordion> + +### Question 2: Nested Promises and Timeouts + +```javascript +setTimeout(() => console.log('timeout 1'), 0); + +Promise.resolve().then(() => { + console.log('promise 1'); + Promise.resolve().then(() => console.log('promise 2')); +}); + +setTimeout(() => console.log('timeout 2'), 0); + +console.log('sync'); +``` + +<Accordion title="Answer"> +**Output:** `sync`, `promise 1`, `promise 2`, `timeout 1`, `timeout 2` + +**Explanation:** +1. First `setTimeout` → callback to Task Queue +2. `Promise.then` → callback to Microtask Queue +3. Second `setTimeout` → callback to Task Queue +4. `console.log('sync')` → runs immediately → "sync" +5. Drain Microtask Queue: + - Run first promise callback → "promise 1" + - This adds another promise to Microtask Queue + - Continue draining → "promise 2" +6. Microtask Queue empty, process Task Queue: + - First timeout → "timeout 1" + - Second timeout → "timeout 2" +</Accordion> + +### Question 3: async/await Ordering + +```javascript +async function foo() { + console.log('foo start'); + await Promise.resolve(); + console.log('foo end'); +} + +console.log('script start'); +foo(); +console.log('script end'); +``` + +<Accordion title="Answer"> +**Output:** `script start`, `foo start`, `script end`, `foo end` + +**Explanation:** +1. `console.log('script start')` → "script start" +2. Call `foo()`: + - `console.log('foo start')` → "foo start" + - `await Promise.resolve()` — pauses foo, schedules continuation as microtask +3. `foo()` returns (suspended at await) +4. `console.log('script end')` → "script end" +5. Call stack empty → drain Microtask Queue → resume foo +6. `console.log('foo end')` → "foo end" + +**Key insight:** `await` splits the function. Code before `await` runs synchronously. Code after `await` runs as a microtask. +</Accordion> + +### Question 4: setTimeout in a Loop + +```javascript +for (var i = 0; i < 3; i++) { + setTimeout(() => console.log(i), 0); +} +``` + +<Accordion title="Answer"> +**Output:** `3`, `3`, `3` + +**Explanation:** +- `var` is function-scoped, so there's only ONE `i` variable +- The loop runs synchronously: i=0, i=1, i=2, i=3 (loop ends) +- THEN the callbacks run, and they all see `i = 3` + +**Fix with let:** +```javascript +for (let i = 0; i < 3; i++) { + setTimeout(() => console.log(i), 0); +} +// Output: 0, 1, 2 +``` + +**Fix with closure (IIFE):** +```javascript +for (var i = 0; i < 3; i++) { + ((j) => { + setTimeout(() => console.log(j), 0); + })(i); +} +// Output: 0, 1, 2 +``` + +**Fix with setTimeout's third parameter:** +```javascript +for (var i = 0; i < 3; i++) { + setTimeout((j) => console.log(j), 0, i); +} +// Output: 0, 1, 2 +``` +</Accordion> + +### Question 5: What's Wrong Here? + +```javascript +const start = Date.now(); +setTimeout(() => { + console.log(`Elapsed: ${Date.now() - start}ms`); +}, 1000); + +// Simulate heavy computation +let sum = 0; +for (let i = 0; i < 1000000000; i++) { + sum += i; +} +console.log('Heavy work done'); +``` + +<Accordion title="Answer"> +**Problem:** The timeout will NOT fire after 1000ms! + +The heavy `for` loop blocks the call stack. Even though the timer finishes after 1000ms, the callback cannot run until the call stack is empty. + +**Typical output:** +``` +Heavy work done +Elapsed: 3245ms // Much longer than 1000ms! +``` + +**Lesson:** Never do heavy synchronous work on the main thread. Use: +- Web Workers for CPU-intensive tasks +- Break work into chunks with setTimeout +- Use `requestIdleCallback` for non-critical work +</Accordion> + +### Question 6: Microtask Starvation + +```javascript +function scheduleMicrotask() { + Promise.resolve().then(() => { + console.log('microtask'); + scheduleMicrotask(); + }); +} + +setTimeout(() => console.log('timeout'), 0); +scheduleMicrotask(); +``` + +<Accordion title="Answer"> +**Output:** `microtask`, `microtask`, `microtask`, ... (forever!) + +The timeout callback NEVER runs! + +**Explanation:** +- Each microtask schedules another microtask +- The Event Loop drains the entire microtask queue before moving to tasks +- The microtask queue is never empty +- The timeout callback starves + +**This is a browser freeze!** The page becomes unresponsive because rendering also waits for the microtask queue to drain. +</Accordion> + +--- + +## Common Misconceptions + +<AccordionGroup> + <Accordion title="Misconception 1: 'setTimeout(fn, 0) runs immediately'"> + **Wrong!** Even with 0ms delay, the callback goes to the Task Queue and must wait for: + 1. Current script to complete + 2. All microtasks to drain + 3. Its turn in the queue + + ```javascript + setTimeout(() => console.log('timeout'), 0); + Promise.resolve().then(() => console.log('promise')); + console.log('sync'); + + // Output: sync, promise, timeout (NOT sync, timeout, promise) + ``` + </Accordion> + + <Accordion title="Misconception 2: 'setTimeout delay is guaranteed'"> + **Wrong!** The delay is a MINIMUM wait time, not a guarantee. + + If the call stack is busy or the Task Queue has items ahead, the actual delay will be longer. + + ```javascript + setTimeout(() => console.log('A'), 100); + setTimeout(() => console.log('B'), 100); + + // Heavy work takes 500ms + for (let i = 0; i < 1e9; i++) {} + + // Both A and B fire at ~500ms, not 100ms + ``` + </Accordion> + + <Accordion title="Misconception 3: 'JavaScript is asynchronous'"> + **Partially wrong!** JavaScript itself is single-threaded and synchronous. + + The asynchronous behavior comes from: + - The **runtime environment** (browser/Node.js) + - **Web APIs** that run in separate threads + - The **Event Loop** that coordinates callbacks + + JavaScript code runs synchronously, one line at a time. The magic is that it can delegate work to the environment. + </Accordion> + + <Accordion title="Misconception 4: 'The Event Loop is part of JavaScript'"> + **Wrong!** The Event Loop is NOT defined in the ECMAScript specification. + + It's defined in the HTML specification (for browsers) and implemented by the runtime environment. Different environments (browsers, Node.js, Deno) have different implementations. + </Accordion> + + <Accordion title="Misconception 5: 'setInterval is accurate'"> + **Wrong!** setInterval can drift, skip callbacks, or have inconsistent timing. + + - If a callback takes longer than the interval, callbacks queue up + - Browsers may throttle timers in background tabs + - Timer precision is limited (especially on mobile) + + For precise timing, use nested setTimeout or requestAnimationFrame. + </Accordion> +</AccordionGroup> + +--- + +## Blocking the Event Loop + +### What Happens When You Block? + +When synchronous code runs for a long time, EVERYTHING stops: + +```javascript +// This freezes the entire page! +button.addEventListener('click', () => { + // Heavy synchronous work + for (let i = 0; i < 10000000000; i++) { + // ... computation + } +}); +``` + +**Consequences:** +- UI freezes (can't click, scroll, or type) +- Animations stop +- setTimeout/setInterval callbacks delayed +- Promises can't resolve +- Page becomes unresponsive + +### Solutions + +<Tabs> + <Tab title="Web Workers"> + Move heavy computation to a separate thread using [Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API): + + ```javascript + // main.js + const worker = new Worker('worker.js'); + + worker.postMessage({ data: largeArray }); + + worker.onmessage = (event) => { + console.log('Result:', event.data); + }; + + // worker.js + self.onmessage = (event) => { + const result = heavyComputation(event.data); + self.postMessage(result); + }; + ``` + </Tab> + + <Tab title="Chunking with setTimeout"> + Break work into smaller chunks: + + ```javascript + function processInChunks(items, process, chunkSize = 100) { + let index = 0; + + function doChunk() { + const end = Math.min(index + chunkSize, items.length); + + for (; index < end; index++) { + process(items[index]); + } + + if (index < items.length) { + setTimeout(doChunk, 0); // Yield to event loop + } + } + + doChunk(); + } + + // Now UI stays responsive between chunks + processInChunks(hugeArray, item => compute(item)); + ``` + </Tab> + + <Tab title="requestIdleCallback"> + Run code during browser idle time with [`requestIdleCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback): + + ```javascript + function doNonCriticalWork(deadline) { + while (deadline.timeRemaining() > 0 && tasks.length > 0) { + const task = tasks.shift(); + task(); + } + + if (tasks.length > 0) { + requestIdleCallback(doNonCriticalWork); + } + } + + requestIdleCallback(doNonCriticalWork); + ``` + </Tab> +</Tabs> + +--- + +## Rendering and the Event Loop + +### Where Does Rendering Fit? + +The browser tries to render at 60fps (every ~16.67ms). Rendering happens **between tasks**, after microtasks: + +``` +┌─────────────────────────────────────────────────────┐ +│ One Frame (~16.67ms) │ +├─────────────────────────────────────────────────────┤ +│ 1. Task (from Task Queue) │ +│ 2. All Microtasks │ +│ 3. requestAnimationFrame callbacks │ +│ 4. Style calculation │ +│ 5. Layout │ +│ 6. Paint │ +│ 7. Composite │ +└─────────────────────────────────────────────────────┘ +``` + +### Why 60fps Matters + +| FPS | Frame Time | User Experience | +|-----|------------|-----------------| +| 60 | 16.67ms | Smooth, responsive | +| 30 | 33.33ms | Noticeable lag | +| 15 | 66.67ms | Very choppy | +| < 10 | > 100ms | Unusable | + +If your JavaScript takes longer than ~16ms, you'll miss frames and the UI will feel janky. + +### Using requestAnimationFrame for Visual Updates + +Use rAF to avoid layout thrashing (reading and writing DOM in a way that forces multiple reflows): + +```javascript +// Bad: Read-write-read pattern forces multiple layouts +console.log(element.offsetWidth); // Read (forces layout) +element.style.width = '100px'; // Write +console.log(element.offsetHeight); // Read (forces layout AGAIN!) +element.style.height = '200px'; // Write + +// Good: Batch reads together, then defer writes to rAF +const width = element.offsetWidth; // Read +const height = element.offsetHeight; // Read (same layout calculation) + +requestAnimationFrame(() => { + // Writes happen right before next paint + element.style.width = width + 100 + 'px'; + element.style.height = height + 100 + 'px'; +}); +``` + +--- + +## Common Bugs and Pitfalls + +<AccordionGroup> + <Accordion title="1. Forgetting to clearInterval"> + ```javascript + // BUG: Memory leak! + function startPolling() { + setInterval(() => { + fetchData(); + }, 5000); + } + + // If called multiple times, intervals stack up! + startPolling(); + startPolling(); // Now 2 intervals running! + + // FIX: Store and clear + let pollInterval; + + function startPolling() { + stopPolling(); // Clear any existing interval + pollInterval = setInterval(fetchData, 5000); + } + + function stopPolling() { + if (pollInterval) { + clearInterval(pollInterval); + pollInterval = null; + } + } + ``` + </Accordion> + + <Accordion title="2. Race Conditions with setTimeout"> + ```javascript + // BUG: Responses may arrive out of order + let searchInput = document.getElementById('search'); + + searchInput.addEventListener('input', () => { + setTimeout(() => { + fetch(`/search?q=${searchInput.value}`) + .then(res => displayResults(res)); + }, 300); + }); + + // FIX: Cancel previous timeout (debounce) + let timeoutId; + searchInput.addEventListener('input', () => { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + fetch(`/search?q=${searchInput.value}`) + .then(res => displayResults(res)); + }, 300); + }); + ``` + </Accordion> + + <Accordion title="3. this Binding in Timer Callbacks"> + ```javascript + // BUG: 'this' is wrong + const obj = { + name: 'Alice', + greet() { + setTimeout(function() { + console.log(`Hello, ${this.name}`); // undefined! + }, 100); + } + }; + + // FIX 1: Arrow function + const obj1 = { + name: 'Alice', + greet() { + setTimeout(() => { + console.log(`Hello, ${this.name}`); // "Alice" + }, 100); + } + }; + + // FIX 2: bind + const obj2 = { + name: 'Alice', + greet() { + setTimeout(function() { + console.log(`Hello, ${this.name}`); + }.bind(this), 100); + } + }; + ``` + </Accordion> + + <Accordion title="4. Closure Issues in Loops"> + ```javascript + // BUG: All callbacks see final value + for (var i = 0; i < 3; i++) { + setTimeout(() => console.log(i), 100); + } + // Output: 3, 3, 3 + + // FIX 1: Use let + for (let i = 0; i < 3; i++) { + setTimeout(() => console.log(i), 100); + } + // Output: 0, 1, 2 + + // FIX 2: Pass as argument + for (var i = 0; i < 3; i++) { + setTimeout((j) => console.log(j), 100, i); + } + // Output: 0, 1, 2 + ``` + </Accordion> + + <Accordion title="5. Assuming Timer Precision"> + ```javascript + // BUG: Assuming exact timing + function measureTime() { + const start = Date.now(); + + setTimeout(() => { + const elapsed = Date.now() - start; + console.log(`Exactly 1000ms? ${elapsed === 1000}`); + // Almost always false! + }, 1000); + } + + // REALITY: Always allow for variance + function measureTime() { + const start = Date.now(); + const expected = 1000; + const tolerance = 50; // Allow 50ms variance + + setTimeout(() => { + const elapsed = Date.now() - start; + const withinTolerance = Math.abs(elapsed - expected) <= tolerance; + console.log(`Within tolerance? ${withinTolerance}`); + }, expected); + } + ``` + </Accordion> +</AccordionGroup> + +--- + +## Interactive Visualization Tool + +The best way to truly understand the Event Loop is to **see it in action**. + +<Card title="Loupe - Event Loop Visualizer" icon="play" href="https://latentflip.com/loupe/"> + Created by Philip Roberts (author of the famous "What the heck is the event loop anyway?" talk). This tool lets you write JavaScript code and watch how it moves through the call stack, Web APIs, and callback queue in real-time. +</Card> + +**Try this code in Loupe:** + +```javascript +console.log('Start'); + +setTimeout(function timeout() { + console.log('Timeout'); +}, 2000); + +Promise.resolve().then(function promise() { + console.log('Promise'); +}); + +console.log('End'); +``` + +Watch how: +1. Synchronous code runs first +2. setTimeout goes to Web APIs +3. Promise callback goes to microtask queue +4. Microtasks run before the timeout callback + +--- + +## Key Takeaways + +<Info> +**The key things to remember:** + +1. **JavaScript is single-threaded** — only one thing runs at a time on the call stack + +2. **The Event Loop enables async** — it coordinates between the call stack and callback queues + +3. **Web APIs run in separate threads** — timers, network requests, and events are handled by the browser + +4. **Microtasks > Tasks** — Promise callbacks ALWAYS run before setTimeout callbacks + +5. **setTimeout delay is a minimum** — actual timing depends on call stack and queue state + +6. **setInterval can drift** — use nested setTimeout for precise timing + +7. **requestAnimationFrame for animations** — syncs with browser refresh rate, pauses in background + +8. **Never block the main thread** — long sync operations freeze the entire UI + +9. **Microtasks can starve tasks** — infinite microtask loops prevent rendering + +10. **The Event Loop isn't JavaScript** — it's part of the runtime environment (browser/Node.js) +</Info> + +--- + +## Test Your Knowledge + +<AccordionGroup> + <Accordion title="Question 1: What is the Event Loop's main job?"> + **Answer:** The Event Loop's job is to monitor the call stack and the callback queues. When the call stack is empty, it takes the first callback from the microtask queue (if any), or the task queue, and pushes it onto the call stack for execution. + + It enables JavaScript to be non-blocking despite being single-threaded. + </Accordion> + + <Accordion title="Question 2: Why do Promises run before setTimeout?"> + **Answer:** Promise callbacks go to the **Microtask Queue**, while setTimeout callbacks go to the **Task Queue** (macrotask queue). + + The Event Loop always drains the entire microtask queue before taking the next task from the task queue. So Promise callbacks always have priority. + </Accordion> + + <Accordion title="Question 3: What's the output of this code?"> + ```javascript + setTimeout(() => console.log('A'), 0); + Promise.resolve().then(() => console.log('B')); + Promise.resolve().then(() => { + console.log('C'); + setTimeout(() => console.log('D'), 0); + }); + console.log('E'); + ``` + + **Answer:** `E`, `B`, `C`, `A`, `D` + + 1. `E` — synchronous + 2. `B` — first microtask + 3. `C` — second microtask (also schedules timeout D) + 4. `A` — first timeout + 5. `D` — second timeout (scheduled during microtask C) + </Accordion> + + <Accordion title="Question 4: When should you use requestAnimationFrame?"> + **Answer:** Use `requestAnimationFrame` for: + + - Visual animations + - DOM updates that need to be smooth + - Anything that should sync with the browser's refresh rate + + **Don't use** it for: + - Non-visual delayed execution (use setTimeout) + - Repeated non-visual tasks (use setInterval or setTimeout) + - Heavy computation (use Web Workers) + </Accordion> + + <Accordion title="Question 5: What's wrong with this code?"> + ```javascript + setInterval(async () => { + const response = await fetch('/api/data'); + const data = await response.json(); + updateUI(data); + }, 1000); + ``` + + **Answer:** If the fetch takes longer than 1 second, multiple requests will be in flight simultaneously, potentially causing race conditions and overwhelming the server. + + **Better approach:** + ```javascript + async function poll() { + const response = await fetch('/api/data'); + const data = await response.json(); + updateUI(data); + setTimeout(poll, 1000); // Schedule next AFTER completion + } + poll(); + ``` + </Accordion> + + <Accordion title="Question 6: How can you yield to the Event Loop in a long-running task?"> + **Answer:** Several approaches: + + ```javascript + // 1. setTimeout (schedules a task) + await new Promise(resolve => setTimeout(resolve, 0)); + + // 2. queueMicrotask (schedules a microtask) + await new Promise(resolve => queueMicrotask(resolve)); + + // 3. requestAnimationFrame (syncs with rendering) + await new Promise(resolve => requestAnimationFrame(resolve)); + + // 4. requestIdleCallback (runs during idle time) + await new Promise(resolve => requestIdleCallback(resolve)); + ``` + + Each has different timing and use cases. setTimeout is most common for yielding. + </Accordion> +</AccordionGroup> + +--- + +## Related Concepts + +<CardGroup cols={2}> + <Card title="Call Stack" icon="layer-group" href="/concepts/call-stack"> + Deep dive into how JavaScript tracks function execution + </Card> + <Card title="Promises" icon="handshake" href="/concepts/promises"> + Understanding Promise-based asynchronous patterns + </Card> + <Card title="async/await" icon="clock" href="/concepts/async-await"> + Modern syntax for working with Promises + </Card> + <Card title="JavaScript Engines" icon="gear" href="/concepts/javascript-engines"> + How V8 and other engines execute your code + </Card> +</CardGroup> + +--- + +## Reference + +<CardGroup cols={2}> + <Card title="JavaScript Execution Model — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Execution_model"> + Official MDN documentation on the JavaScript runtime, event loop, and execution contexts. + </Card> + <Card title="setTimeout — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/setTimeout"> + Complete reference for setTimeout including syntax, parameters, and the minimum delay behavior. + </Card> + <Card title="setInterval — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/setInterval"> + Documentation for repeated timed callbacks with usage patterns and gotchas. + </Card> + <Card title="requestAnimationFrame — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame"> + Browser-optimized animation timing API that syncs with display refresh rate. + </Card> +</CardGroup> + +## Articles + +<CardGroup cols={2}> + <Card title="JavaScript Visualized: Event Loop" icon="newspaper" href="https://dev.to/lydiahallie/javascript-visualized-event-loop-3dif"> + Lydia Hallie's famous visual explanation with animated GIFs showing exactly how the event loop works. + </Card> + <Card title="Tasks, microtasks, queues and schedules" icon="newspaper" href="https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/"> + Jake Archibald's definitive deep-dive with interactive examples. The go-to resource for understanding tasks vs microtasks. + </Card> + <Card title="The JavaScript Event Loop" icon="newspaper" href="https://flaviocopes.com/javascript-event-loop/"> + Flavio Copes' clear explanation with excellent code examples showing Promise vs setTimeout behavior. + </Card> + <Card title="setTimeout and setInterval" icon="newspaper" href="https://javascript.info/settimeout-setinterval"> + Comprehensive JavaScript.info guide covering timers, cancellation, nested setTimeout, and the 4ms minimum delay. + </Card> + <Card title="Using requestAnimationFrame" icon="newspaper" href="https://css-tricks.com/using-requestanimationframe/"> + Chris Coyier's practical guide to smooth animations with requestAnimationFrame, including polyfills and examples. + </Card> + <Card title="Why not to use setInterval" icon="newspaper" href="https://dev.to/akanksha_9560/why-not-to-use-setinterval--2na9"> + Deep dive into setInterval's problems with drift, async operations, and why nested setTimeout is often better. + </Card> +</CardGroup> + +## Tools + +<Card title="Loupe - Event Loop Visualizer" icon="play" href="https://latentflip.com/loupe/"> + Interactive tool by Philip Roberts to visualize how the call stack, Web APIs, and callback queue work together. Write code and watch it execute step by step. +</Card> + +## Videos + +<CardGroup cols={2}> + <Card title="What the heck is the event loop anyway?" icon="video" href="https://www.youtube.com/watch?v=8aGhZQkoFbQ"> + Philip Roberts' legendary JSConf EU talk that made the event loop accessible to everyone. A must-watch for JavaScript developers. + </Card> + <Card title="In The Loop" icon="video" href="https://www.youtube.com/watch?v=cCOL7MC4Pl0"> + Jake Archibald's JSConf.Asia talk diving deeper into tasks, microtasks, and rendering. The perfect follow-up to Philip Roberts' talk. + </Card> + <Card title="TRUST ISSUES with setTimeout()" icon="video" href="https://youtu.be/nqsPmuicJJc"> + Akshay Saini explains why you can't trust setTimeout's timing and how the event loop actually handles timers. + </Card> +</CardGroup> diff --git a/docs/concepts/factories-classes.mdx b/docs/concepts/factories-classes.mdx new file mode 100644 index 00000000..8d48d262 --- /dev/null +++ b/docs/concepts/factories-classes.mdx @@ -0,0 +1,2033 @@ +--- +title: "Factories and Classes: Creating Objects Efficiently in JavaScript" +sidebarTitle: "Factories and Classes: Creating Objects Efficiently" +description: "Learn JavaScript factory functions and ES6 classes. Understand constructors, prototypes, private fields, inheritance, and when to use each pattern." +--- + +How do you create hundreds of similar objects without copy-pasting? How do game developers spawn thousands of enemies? How does JavaScript let you build blueprints for objects? + +```javascript +// Factory function — returns a new object each time +function createPlayer(name) { + return { + name, + health: 100, + attack() { + return `${this.name} attacks!` + } + } +} + +// Class — a blueprint for creating objects +class Enemy { + constructor(name) { + this.name = name + this.health = 100 + } + + attack() { + return `${this.name} attacks!` + } +} + +// Both create objects the same way +const player = createPlayer("Alice") // Factory +const enemy = new Enemy("Goblin") // Class + +console.log(player.attack()) // "Alice attacks!" +console.log(enemy.attack()) // "Goblin attacks!" +``` + +**Factories** and **[Classes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes)** are two patterns for creating objects efficiently. A factory function is a regular function that returns a new object. A class is a blueprint that uses the [`class`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/class) keyword and the [`new`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new) operator. Both achieve the same goal, but they work differently and have different strengths. + +<Info> +**What you'll learn in this guide:** +- How to create objects using factory functions +- How constructor functions and the `new` keyword work +- ES6 class syntax and what "syntactic sugar" means +- Private fields (#) and how they differ from closures +- Static methods, getters, and setters +- Inheritance with `extends` and `super` +- Factory composition vs class inheritance +- When to use factories vs classes +</Info> + +<Warning> +**Prerequisites:** This guide assumes you understand [Object Creation & Prototypes](/concepts/object-creation-prototypes) and [this, call, apply, bind](/concepts/this-call-apply-bind). If those concepts are new to you, read those guides first! +</Warning> + +--- + +## Why Do We Need Object Blueprints? + +### The Manual Approach (Don't Do This) + +Let's say you're building an RPG game. You need player characters: + +```javascript +// Creating players manually — tedious and error-prone +const player1 = { + name: "Alice", + health: 100, + level: 1, + attack() { + return `${this.name} attacks for ${10 + this.level * 2} damage!`; + }, + takeDamage(amount) { + this.health -= amount; + if (this.health <= 0) { + return `${this.name} has been defeated!`; + } + return `${this.name} has ${this.health} health remaining.`; + } +}; + +const player2 = { + name: "Bob", + health: 100, + level: 1, + attack() { + return `${this.name} attacks for ${10 + this.level * 2} damage!`; + }, + takeDamage(amount) { + this.health -= amount; + if (this.health <= 0) { + return `${this.name} has been defeated!`; + } + return `${this.name} has ${this.health} health remaining.`; + } +}; + +// ... 50 more players with the same code copied ... +``` + +### What's Wrong With This? + +| Problem | Why It's Bad | +|---------|--------------| +| **Repetition** | Same code copied over and over | +| **Error-prone** | Easy to make typos or forget properties | +| **Hard to maintain** | Change one thing? Change it everywhere | +| **No consistency** | Nothing enforces that all players have the same structure | +| **Memory waste** | Each object has its own copy of the methods | + +### What We Need + +We need a way to: +- Define the structure **once** +- Create as many objects as we need +- Ensure all objects have the same properties and methods +- Make changes in **one place** that affect all objects + +### The Assembly Line Analogy + +Think about how real-world manufacturing works: + +- **Hand-crafting** each item individually is slow, inconsistent, and doesn't scale +- **Assembly lines** (factories) take specifications and produce products efficiently +- **Blueprints/molds** define the template once, then stamp out identical copies + +JavaScript gives us the same options: + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ THREE WAYS TO CREATE OBJECTS │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ MANUAL CREATION Like hand-carving each chess piece │ +│ ─────────────── Tedious, error-prone, inconsistent │ +│ const obj = { ... } │ +│ │ +│ ───────────────────────────────────────────────────────────────────── │ +│ │ +│ FACTORY FUNCTION Like an assembly line │ +│ ──────────────── Put in specs → Get product │ +│ Flexible, no special keywords │ +│ createPlayer("Alice") │ +│ │ │ +│ ▼ │ +│ ┌─────────────┐ │ +│ │ Player │ ← New object returned │ +│ │ {name...} │ │ +│ └─────────────┘ │ +│ │ +│ ───────────────────────────────────────────────────────────────────── │ +│ │ +│ CLASS / CONSTRUCTOR Like a blueprint or mold │ +│ ─────────────────── Define template → Stamp out copies │ +│ Uses `new`, supports `instanceof` │ +│ new Player("Alice") │ +│ │ │ +│ ▼ │ +│ ┌─────────────┐ │ +│ │ Player │ ← Instance created from blueprint │ +│ │ {name...} │ │ +│ └─────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +Both factories and classes solve the same problem. They just do it differently. Let's explore each approach. + +--- + +## What is a Factory Function in JavaScript? + +A **factory function** is a regular JavaScript function that creates and returns a new object each time it's called. Unlike constructors or classes, factory functions don't require the `new` keyword. They can use `this` in returned methods (like simple objects do), or use [closures](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures) to avoid `this` entirely, giving you flexibility that classes don't offer. + +### Basic Factory Function + +Think of it like an assembly line. You put in the specifications, and it produces the product: + +```javascript +// A simple factory function +function createPlayer(name) { + return { + name: name, + health: 100, + level: 1, + attack() { + return `${this.name} attacks for ${10 + this.level * 2} damage!`; + }, + takeDamage(amount) { + this.health -= amount; + if (this.health <= 0) { + return `${this.name} has been defeated!`; + } + return `${this.name} has ${this.health} health remaining.`; + } + }; +} + +// Creating players is now easy! +const alice = createPlayer("Alice"); +const bob = createPlayer("Bob"); +const charlie = createPlayer("Charlie"); + +console.log(alice.attack()); // "Alice attacks for 12 damage!" +console.log(bob.takeDamage(30)); // "Bob has 70 health remaining." +``` + +### Factory with Multiple Parameters + +```javascript +function createEnemy(name, health, attackPower) { + return { + name, // Shorthand: same as name: name + health, + attackPower, + isAlive: true, + + attack(target) { + return `${this.name} attacks ${target.name} for ${this.attackPower} damage!`; + }, + + takeDamage(amount) { + this.health -= amount; + if (this.health <= 0) { + this.health = 0; + this.isAlive = false; + return `${this.name} has been defeated!`; + } + return `${this.name} has ${this.health} health remaining.`; + } + }; +} + +// Create different types of enemies +const goblin = createEnemy("Goblin", 50, 10); +const dragon = createEnemy("Dragon", 500, 50); +const boss = createEnemy("Dark Lord", 1000, 100); + +console.log(goblin.attack(dragon)); // "Goblin attacks Dragon for 10 damage!" +console.log(dragon.takeDamage(100)); // "Dragon has 400 health remaining." +``` + +### Factory with Configuration Object + +For many options, use a configuration object: + +```javascript +function createCharacter(config) { + // Default values + const defaults = { + name: "Unknown", + health: 100, + maxHealth: 100, + level: 1, + experience: 0, + attackPower: 10, + defense: 5 + }; + + // Merge defaults with provided config + const settings = { ...defaults, ...config }; + + return { + ...settings, + + attack(target) { + const damage = Math.max(0, this.attackPower - target.defense); + return `${this.name} deals ${damage} damage to ${target.name}!`; + }, + + heal(amount) { + this.health = Math.min(this.maxHealth, this.health + amount); + return `${this.name} healed to ${this.health} health.`; + }, + + gainExperience(amount) { + this.experience += amount; + if (this.experience >= this.level * 100) { + this.level++; + this.experience = 0; + this.attackPower += 5; + return `${this.name} leveled up to ${this.level}!`; + } + return `${this.name} gained ${amount} XP.`; + } + }; +} + +// Create characters with different configurations +const warrior = createCharacter({ + name: "Warrior", + health: 150, + maxHealth: 150, + attackPower: 20, + defense: 10 +}); + +const mage = createCharacter({ + name: "Mage", + health: 80, + maxHealth: 80, + attackPower: 30, + defense: 3 +}); + +// Only override what you need +const villager = createCharacter({ name: "Villager" }); +``` + +### Factory with Private Variables (Closures) + +A powerful feature of factory functions is creating **truly private** variables using [closures](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures): + +```javascript +function createBankAccount(ownerName, initialBalance = 0) { + // Private variables — NOT accessible from outside + let balance = initialBalance; + const transactionHistory = []; + + // Private function + function recordTransaction(type, amount) { + transactionHistory.push({ + type, + amount, + balance, + date: new Date().toISOString() + }); + } + + // Initialize + recordTransaction("opening", initialBalance); + + // Return public interface + return { + owner: ownerName, + + deposit(amount) { + if (amount <= 0) { + throw new Error("Deposit amount must be positive"); + } + balance += amount; + recordTransaction("deposit", amount); + return `Deposited $${amount}. New balance: $${balance}`; + }, + + withdraw(amount) { + if (amount <= 0) { + throw new Error("Withdrawal amount must be positive"); + } + if (amount > balance) { + throw new Error("Insufficient funds"); + } + balance -= amount; + recordTransaction("withdrawal", amount); + return `Withdrew $${amount}. New balance: $${balance}`; + }, + + getBalance() { + return balance; + }, + + getStatement() { + return transactionHistory.map(t => + `${t.date}: ${t.type} $${t.amount} (Balance: $${t.balance})` + ).join('\n'); + } + }; +} + +const account = createBankAccount("Alice", 1000); + +console.log(account.deposit(500)); // "Deposited $500. New balance: $1500" +console.log(account.withdraw(200)); // "Withdrew $200. New balance: $1300" +console.log(account.getBalance()); // 1300 + +// Trying to access private variables — FAILS! +console.log(account.balance); // undefined +console.log(account.transactionHistory); // undefined + +// Can't cheat! +account.balance = 1000000; // Does nothing useful +console.log(account.getBalance()); // Still 1300 +``` + +<Tip> +**Why is this private?** The variables `balance` and `transactionHistory` exist only inside the factory function. The returned object's methods can access them through **closure**, but nothing outside can. This is true encapsulation! +</Tip> + +### Factory Creating Different Types + +Factories can return different object types based on input: + +```javascript +function createWeapon(type) { + const weapons = { + sword: { + name: "Iron Sword", + damage: 25, + speed: "medium", + attack() { + return `Slash with ${this.name} for ${this.damage} damage!`; + } + }, + bow: { + name: "Longbow", + damage: 20, + speed: "fast", + range: 100, + attack() { + return `Fire an arrow for ${this.damage} damage from ${this.range}m away!`; + } + }, + staff: { + name: "Magic Staff", + damage: 35, + speed: "slow", + manaCost: 10, + attack() { + return `Cast a spell for ${this.damage} damage! (Costs ${this.manaCost} mana)`; + } + } + }; + + if (!weapons[type]) { + throw new Error(`Unknown weapon type: ${type}`); + } + + return { ...weapons[type] }; // Return a copy +} + +const sword = createWeapon("sword"); +const bow = createWeapon("bow"); +const staff = createWeapon("staff"); + +console.log(sword.attack()); // "Slash with Iron Sword for 25 damage!" +console.log(bow.attack()); // "Fire an arrow for 20 damage from 100m away!" +console.log(staff.attack()); // "Cast a spell for 35 damage! (Costs 10 mana)" +``` + +### When to Use Factory Functions + +<AccordionGroup> + <Accordion title="You need truly private data"> + Factory functions with closures provide **real** privacy. Variables inside the factory can't be accessed or modified from outside, not even through hacks or reflection. + </Accordion> + + <Accordion title="You don't need instanceof checks"> + Factory-created objects are plain objects. They don't have a special prototype chain, so `instanceof` won't work. If you need to check object types, use classes instead. + </Accordion> + + <Accordion title="You want flexibility over structure"> + Factories can return different types of objects, partially constructed objects, or even primitives. Classes always return instances of that class. + </Accordion> + + <Accordion title="You prefer functional programming"> + Factory functions fit well with functional programming patterns. They're just functions that return data. + </Accordion> +</AccordionGroup> + +--- + +## How Do Constructor Functions Work? + +A **constructor function** is a regular JavaScript function designed to be called with the [`new`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new) keyword. When invoked with `new`, it creates a new object, binds `this` to that object, and returns it automatically. Constructor names conventionally start with a capital letter to distinguish them from regular functions. This was the standard way to create objects before ES6 classes. + +### Basic Constructor Function + +```javascript +// Convention: Constructor names start with a capital letter +function Player(name) { + // 'this' refers to the new object being created + this.name = name; + this.health = 100; + this.level = 1; + + this.attack = function() { + return `${this.name} attacks for ${10 + this.level * 2} damage!`; + }; +} + +// Create instances with 'new' +const alice = new Player("Alice"); +const bob = new Player("Bob"); + +console.log(alice.name); // "Alice" +console.log(bob.attack()); // "Bob attacks for 12 damage!" +console.log(alice instanceof Player); // true +``` + +### The `new` Keyword — What It Actually Does + +When you call `new Player("Alice")`, JavaScript performs **4 steps**: + +<Steps> + <Step title="Create a new empty object"> + JavaScript creates a fresh object: `const obj = {}` + </Step> + <Step title="Link the prototype"> + Sets `obj.[[Prototype]]` to `Constructor.prototype`, establishing the prototype chain + </Step> + <Step title="Execute the constructor"> + Runs the constructor with `this` bound to the new object + </Step> + <Step title="Return the object"> + Returns `obj` automatically (unless the constructor explicitly returns a different non-null object; primitive return values are ignored) + </Step> +</Steps> + +<Tip> +**Want to dive deeper?** For a detailed explanation of how `new` works under the hood, including how to simulate it yourself, see [Object Creation & Prototypes](/concepts/object-creation-prototypes). +</Tip> + +### Adding Methods to the [Prototype](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Object_prototypes) + +There's a problem with our constructor: **each instance gets its own copy of methods**: + +```javascript +function Player(name) { + this.name = name; + this.health = 100; + + // BAD: Every player gets their own copy of this function + this.attack = function() { + return `${this.name} attacks!`; + }; +} + +const p1 = new Player("Alice"); +const p2 = new Player("Bob"); + +// These are different functions! +console.log(p1.attack === p2.attack); // false + +// 1000 players = 1000 copies of attack function = wasted memory! +``` + +The solution is to put methods on the **[prototype](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain)**: + +```javascript +function Player(name) { + this.name = name; + this.health = 100; + // Don't put methods here! +} + +// Add methods to the prototype — shared by all instances +Player.prototype.attack = function() { + return `${this.name} attacks!`; +}; + +Player.prototype.takeDamage = function(amount) { + this.health -= amount; + return `${this.name} has ${this.health} health.`; +}; + +const p1 = new Player("Alice"); +const p2 = new Player("Bob"); + +// Now they share the same function! +console.log(p1.attack === p2.attack); // true + +// 1000 players = 1 copy of attack function = efficient! +``` + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ PROTOTYPE CHAIN │ +│ │ +│ Player.prototype │ +│ ┌─────────────────────────┐ │ +│ │ attack: function() │ │ +│ │ takeDamage: function() │◄──── Shared by all instances │ +│ └─────────────────────────┘ │ +│ ▲ │ +│ │ [[Prototype]] │ +│ │ │ +│ ┌──────────┴──────────┐ │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌─────────┐ ┌─────────┐ │ +│ │ p1 │ │ p2 │ │ +│ │─────────│ │─────────│ │ +│ │name: │ │name: │ │ +│ │"Alice" │ │"Bob" │ │ +│ │health: │ │health: │ │ +│ │100 │ │100 │ │ +│ └─────────┘ └─────────┘ │ +│ │ +│ Each instance has its own data, but shares methods via prototype │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +### The [`instanceof`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/instanceof) Operator + +`instanceof` checks if an object was created by a constructor: + +```javascript +function Player(name) { + this.name = name; +} + +function Enemy(name) { + this.name = name; +} + +const alice = new Player("Alice"); +const goblin = new Enemy("Goblin"); + +console.log(alice instanceof Player); // true +console.log(alice instanceof Enemy); // false +console.log(goblin instanceof Enemy); // true +console.log(goblin instanceof Player); // false + +// Both are instances of Object +console.log(alice instanceof Object); // true +console.log(goblin instanceof Object); // true +``` + +### The Problem: Forgetting `new` + +```javascript +function Player(name) { + this.name = name; + this.health = 100; +} + +// Oops! Forgot 'new' +const alice = Player("Alice"); + +console.log(alice); // undefined (function returned nothing) +console.log(name); // "Alice" — LEAKED to global scope! +console.log(health); // 100 — ALSO leaked! + +// In strict mode, this would throw an error instead +// 'use strict'; +// Player("Alice"); // TypeError: Cannot set property 'name' of undefined +``` + +<Warning> +Always use `new` with constructor functions! Without it, `this` refers to the global object (or `undefined` in strict mode), causing bugs that are hard to track down. +</Warning> + +--- + +## What Are ES6 Classes in JavaScript? + +An **[ES6 class](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes)** is JavaScript's modern syntax for creating constructor functions and prototypes. Introduced in ECMAScript 2015, classes provide a cleaner, more familiar syntax for object-oriented programming while working exactly the same as constructor functions under the hood. They're often called "syntactic sugar." Classes use the `class` keyword and require the `new` operator to create instances. + +### Basic Class Syntax + +```javascript +class Player { + constructor(name) { + this.name = name; + this.health = 100; + this.level = 1; + } + + attack() { + return `${this.name} attacks for ${10 + this.level * 2} damage!`; + } + + takeDamage(amount) { + this.health -= amount; + if (this.health <= 0) { + return `${this.name} has been defeated!`; + } + return `${this.name} has ${this.health} health remaining.`; + } +} + +const alice = new Player("Alice"); +console.log(alice.attack()); // "Alice attacks for 12 damage!" +console.log(alice instanceof Player); // true +``` + +### Classes Are "Syntactic Sugar" + +Classes don't add new functionality. They're just a nicer way to write constructor functions. Under the hood, they work exactly the same: + +<Tabs> + <Tab title="ES6 Class"> + ```javascript + class Enemy { + constructor(name, health) { + this.name = name; + this.health = health; + } + + attack() { + return `${this.name} attacks!`; + } + + static createBoss(name) { + return new Enemy(name, 1000); + } + } + ``` + </Tab> + + <Tab title="Equivalent ES5"> + ```javascript + function Enemy(name, health) { + this.name = name; + this.health = health; + } + + Enemy.prototype.attack = function() { + return `${this.name} attacks!`; + }; + + Enemy.createBoss = function(name) { + return new Enemy(name, 1000); + }; + ``` + </Tab> +</Tabs> + +Both create objects with the same structure: + +```javascript +// Both versions produce: +const goblin = new Enemy("Goblin", 100); + +console.log(typeof Enemy); // "function" (classes ARE functions!) +console.log(goblin.constructor === Enemy); // true +console.log(goblin.__proto__ === Enemy.prototype); // true +``` + +### Class Syntax Breakdown + +```javascript +class Character { + // Class field (public property with default value) + level = 1; + experience = 0; + + // Constructor — called when you use 'new' + constructor(name, health = 100) { + this.name = name; + this.health = health; + } + + // Instance method — available on all instances + attack() { + return `${this.name} attacks!`; + } + + // Another instance method + heal(amount) { + this.health += amount; + return `${this.name} healed to ${this.health} HP.`; + } + + // Getter — accessed like a property + get isAlive() { + return this.health > 0; + } + + // Setter — assigned like a property + set healthPoints(value) { + this.health = Math.max(0, value); // Can't go below 0 + } + + // Static method — called on the class, not instances + static createHero(name) { + return new Character(name, 150); + } + + // Static property + static MAX_LEVEL = 99; +} + +// Usage +const hero = Character.createHero("Alice"); // Static method +console.log(hero.attack()); // Instance method +console.log(hero.isAlive); // Getter (no parentheses!) +hero.healthPoints = -50; // Setter +console.log(hero.health); // 0 (setter prevented negative) +console.log(Character.MAX_LEVEL); // 99 (static property) +``` + +### [Static](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/static) Methods and Properties + +**Static** members belong to the class itself, not to instances: + +```javascript +class MathUtils { + // Static properties + static PI = 3.14159; + static E = 2.71828; + + // Static methods + static square(x) { + return x * x; + } + + static cube(x) { + return x * x * x; + } + + static randomBetween(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; + } +} + +// Access via class name +console.log(MathUtils.PI); // 3.14159 +console.log(MathUtils.square(5)); // 25 + +// NOT via instances! +const utils = new MathUtils(); +console.log(utils.PI); // undefined +console.log(utils.square); // undefined +``` + +**Common uses for static methods:** +- Factory methods (`User.fromJSON(data)`) +- Utility functions (`Array.isArray(value)`) +- Singleton patterns (`Config.getInstance()`) + +### [Getters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get) and [Setters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/set) + +Getters and setters let you define computed properties and add validation: + +```javascript +class Temperature { + constructor(celsius) { + this._celsius = celsius; // Convention: underscore = "private" + } + + // Getter: accessed like a property + get celsius() { + return this._celsius; + } + + // Setter: assigned like a property + set celsius(value) { + if (value < -273.15) { + throw new Error("Temperature below absolute zero!"); + } + this._celsius = value; + } + + // Computed getter: fahrenheit from celsius + get fahrenheit() { + return this._celsius * 9/5 + 32; + } + + // Computed setter: set celsius from fahrenheit + set fahrenheit(value) { + this.celsius = (value - 32) * 5/9; // Uses celsius setter for validation + } + + // Read-only getter (no setter) + get kelvin() { + return this._celsius + 273.15; + } +} + +const temp = new Temperature(25); + +console.log(temp.celsius); // 25 +console.log(temp.fahrenheit); // 77 +console.log(temp.kelvin); // 298.15 + +temp.fahrenheit = 100; // Set via fahrenheit +console.log(temp.celsius); // ~37.78 (converted) + +// temp.celsius = -300; // Error: Temperature below absolute zero! +// temp.kelvin = 0; // Error: no setter (read-only) +``` + +### [Private Fields (#)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_class_fields) — True Privacy + +ES2020 introduced **private fields** with the `#` prefix. Unlike the `_underscore` convention, these are **truly private**: + +```javascript +class BankAccount { + // Private fields — declared with # + #balance = 0; + #pin; + #transactionHistory = []; + + constructor(ownerName, initialBalance, pin) { + this.ownerName = ownerName; // Public + this.#balance = initialBalance; + this.#pin = pin; + } + + // Private method + #recordTransaction(type, amount) { + this.#transactionHistory.push({ + type, + amount, + balance: this.#balance, + date: new Date() + }); + } + + // Private method for PIN verification + #verifyPin(pin) { + return this.#pin === pin; + } + + // Public methods + deposit(amount) { + if (amount <= 0) throw new Error("Invalid amount"); + this.#balance += amount; + this.#recordTransaction("deposit", amount); + return this.#balance; + } + + withdraw(amount, pin) { + if (!this.#verifyPin(pin)) { + throw new Error("Invalid PIN"); + } + if (amount > this.#balance) { + throw new Error("Insufficient funds"); + } + this.#balance -= amount; + this.#recordTransaction("withdrawal", amount); + return this.#balance; + } + + getBalance(pin) { + if (!this.#verifyPin(pin)) { + throw new Error("Invalid PIN"); + } + return this.#balance; + } +} + +const account = new BankAccount("Alice", 1000, "1234"); + +account.deposit(500); +console.log(account.withdraw(200, "1234")); // 1300 +console.log(account.getBalance("1234")); // 1300 + +// Trying to access private fields — ALL FAIL +// account.#balance; // SyntaxError! +// account.#pin; // SyntaxError! +// account.#verifyPin("1234"); // SyntaxError! + +console.log(account.balance); // undefined (different property) +``` + +### Private Fields (#) vs Closure-Based Privacy + +Both provide true privacy, but they work differently: + +| Feature | Private Fields (#) | Closures (Factory) | +|---------|-------------------|-------------------| +| Syntax | `this.#field` | `let variable` inside function | +| Access error | SyntaxError | Returns `undefined` | +| Memory | Efficient (prototype methods) | Each instance has own methods | +| `instanceof` | Works | Doesn't work | +| Inheritance | Private per class | Not inherited | +| Debugger visibility | Visible but inaccessible | Visible in closure scope | + +```javascript +// Private Fields (#) +class Wallet { + #balance = 0; + + deposit(amount) { this.#balance += amount; } + getBalance() { return this.#balance; } +} + +const w1 = new Wallet(); +const w2 = new Wallet(); +console.log(w1.deposit === w2.deposit); // true (shared via prototype) + +// Closure-based (Factory) +function createWallet() { + let balance = 0; + + return { + deposit(amount) { balance += amount; }, + getBalance() { return balance; } + }; +} + +const w3 = createWallet(); +const w4 = createWallet(); +console.log(w3.deposit === w4.deposit); // false (each has own copy) +``` + +--- + +## Common Mistakes with Factories and Classes + +When working with factories and classes, there are several common pitfalls that trip up developers. Let's look at the most frequent mistakes and how to avoid them. + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ THE 3 MOST COMMON MISTAKES │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ 1. FORGETTING `new` WITH CONSTRUCTORS │ +│ Pollutes global scope or crashes in strict mode │ +│ │ +│ 2. FORGETTING `super()` IN DERIVED CLASSES │ +│ Must call super() before using `this` │ +│ │ +│ 3. CONFUSING `_private` WITH TRULY PRIVATE │ +│ Underscore is just a convention, not enforcement │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### Mistake 1: Forgetting `new` with Constructor Functions + +```javascript +// ❌ WRONG - Forgot 'new', 'this' becomes global object +function Player(name) { + this.name = name; + this.health = 100; +} + +const alice = Player("Alice"); // Missing 'new'! + +console.log(alice); // undefined +console.log(globalThis.name); // "Alice" - leaked to global! +console.log(globalThis.health); // 100 - also leaked! + +// ✓ CORRECT - Always use 'new' with constructors +const bob = new Player("Bob"); +console.log(bob.name); // "Bob" +console.log(bob.health); // 100 +``` + +<Tip> +**Pro tip:** Use ES6 classes instead of constructor functions — they throw an error if you forget `new`: + +```javascript +class Player { + constructor(name) { this.name = name; } +} + +const alice = Player("Alice"); // TypeError: Class constructor Player cannot be invoked without 'new' +``` +</Tip> + +### Mistake 2: Forgetting `super()` in Derived Classes + +```javascript +// ❌ WRONG - Using 'this' before calling super() +class Animal { + constructor(name) { + this.name = name; + } +} + +class Dog extends Animal { + constructor(name, breed) { + this.breed = breed; // ReferenceError: Must call super before accessing 'this' + super(name); + } +} + +// ✓ CORRECT - Call super() first, then use 'this' +class Cat extends Animal { + constructor(name, color) { + super(name); // Initialize parent first + this.color = color; // Now 'this' is available + } +} + +const kitty = new Cat("Whiskers", "orange"); +console.log(kitty.name); // "Whiskers" +console.log(kitty.color); // "orange" +``` + +### Mistake 3: Thinking `_underscore` Means Private + +```javascript +// ❌ WRONG - Underscore is just a naming convention +class BankAccount { + constructor(balance) { + this._balance = balance; // Not actually private! + } + + getBalance() { + return this._balance; + } +} + +const account = new BankAccount(1000); +console.log(account._balance); // 1000 - fully accessible! +account._balance = 999999; // Can be modified! +console.log(account.getBalance()); // 999999 - no protection! + +// ✓ CORRECT - Use private fields (#) for true privacy +class SecureBankAccount { + #balance; // Truly private + + constructor(balance) { + this.#balance = balance; + } + + getBalance() { + return this.#balance; + } +} + +const secure = new SecureBankAccount(1000); +// console.log(secure.#balance); // SyntaxError! +console.log(secure.getBalance()); // 1000 - only accessible via methods +``` + +### Mistake 4: Using `this` Incorrectly in Factory Functions + +```javascript +// ❌ WRONG - 'this' in factory return object can cause issues +function createCounter() { + return { + count: 0, + increment() { + this.count++; // 'this' depends on how the method is called + } + }; +} + +const counter = createCounter(); +counter.increment(); // Works +console.log(counter.count); // 1 + +const increment = counter.increment; +increment(); // 'this' is undefined or global! +console.log(counter.count); // Still 1 - didn't work! + +// ✓ CORRECT - Use closure to avoid 'this' issues +function createSafeCounter() { + let count = 0; // Closure variable + + return { + increment() { + count++; // No 'this' needed + }, + getCount() { + return count; + } + }; +} + +const safeCounter = createSafeCounter(); +const safeIncrement = safeCounter.increment; +safeIncrement(); // Works even when extracted! +console.log(safeCounter.getCount()); // 1 +``` + +<Warning> +**The `this` Trap:** When you extract a method from an object and call it standalone, `this` is no longer bound to the original object. Factory functions that use closures instead of `this` avoid this problem entirely. +</Warning> + +<Tip> +**Arrow Function Class Fields:** In classes, you can use arrow functions as class fields to auto-bind `this`: + +```javascript +class Button { + count = 0; + + // Arrow function automatically binds 'this' to the instance + handleClick = () => { + this.count++; + console.log(`Clicked ${this.count} times`); + }; +} + +const button = new Button(); +const handler = button.handleClick; +handler(); // Works! 'this' is still bound to button +``` + +This is an alternative to manually binding methods with `.bind(this)` in the constructor. +</Tip> + +--- + +## Classic Interview Questions + +<AccordionGroup> + <Accordion title="What's the difference between a factory function and a class?"> + **Answer:** + + | Aspect | Factory Function | ES6 Class | + |--------|-----------------|-----------| + | Syntax | Regular function returning object | `class` keyword | + | `new` keyword | Not required | Required | + | `instanceof` | Doesn't work | Works | + | Privacy | Closures (truly private) | Private fields `#` (truly private) | + | Memory | Each instance has own methods | Methods shared via prototype | + | `this` binding | Can avoid `this` entirely | Must use `this` | + + ```javascript + // Factory - just a function + function createUser(name) { + return { name, greet() { return `Hi, ${name}!` } } + } + + // Class - a blueprint + class User { + constructor(name) { this.name = name } + greet() { return `Hi, ${this.name}!` } + } + + const u1 = createUser("Alice") // No 'new' + const u2 = new User("Bob") // Requires 'new' + ``` + + **Best answer:** Explain both syntax differences AND when to use each. + </Accordion> + + <Accordion title="What does the new keyword do under the hood?"> + **Answer:** + + `new` performs 4 steps: + + 1. **Creates** a new empty object `{}` + 2. **Links** its prototype to `Constructor.prototype` + 3. **Executes** the constructor with `this` bound to the new object + 4. **Returns** the object (unless constructor returns a different object) + + ```javascript + // This is essentially what 'new' does: + function myNew(Constructor, ...args) { + const obj = Object.create(Constructor.prototype) + const result = Constructor.apply(obj, args) + return (typeof result === 'object' && result !== null) ? result : obj + } + ``` + + **Best answer:** Mention all 4 steps and show the simulation code. + </Accordion> + + <Accordion title="How do you achieve true privacy in JavaScript?"> + **Answer:** + + Two ways to achieve **true** privacy: + + **1. Private Fields (`#`) in Classes:** + ```javascript + class BankAccount { + #balance = 0 + deposit(amt) { this.#balance += amt } + getBalance() { return this.#balance } + } + // account.#balance → SyntaxError! + ``` + + **2. Closures in Factory Functions:** + ```javascript + function createBankAccount() { + let balance = 0 + return { + deposit(amt) { balance += amt }, + getBalance() { return balance } + } + } + // account.balance → undefined + ``` + + **Not truly private:** The `_underscore` convention is just a naming hint. Those properties are fully accessible. + + **Best answer:** Distinguish between the `_underscore` convention (not private) and the two truly private approaches. + </Accordion> + + <Accordion title="When would you use composition over inheritance?"> + **Answer:** + + Use **composition** when: + - You need to mix behaviors from multiple sources (a flying fish, a swimming bird) + - The "is-a" relationship doesn't make sense + - You want loose coupling between components + - You need flexibility to change behaviors at runtime + + Use **inheritance** when: + - There's a clear "is-a" hierarchy (Dog is an Animal) + - You need `instanceof` checks + - You want to share implementation, not just interface + + ```javascript + // Inheritance problem: What about a penguin that can't fly? + class Bird { fly() {} } + class Penguin extends Bird { fly() { throw Error("Can't fly!") } } // Awkward! + + // Composition solution: Mix behaviors + const canSwim = (state) => ({ swim() { /*...*/ } }) + const canWalk = (state) => ({ walk() { /*...*/ } }) + + function createPenguin(name) { + const state = { name } + return { ...canSwim(state), ...canWalk(state) } // No fly! + } + ``` + + **Best answer:** Give the "Gorilla-Banana" problem example and show composition code. + </Accordion> +</AccordionGroup> + +--- + +## Common Misconceptions + +<AccordionGroup> + <Accordion title="Misconception: 'Classes in JavaScript work like classes in Java or C#'"> + **Reality:** JavaScript classes are **syntactic sugar** over prototypes. Under the hood, they still use prototype-based inheritance, not classical inheritance. + + ```javascript + class Player { + constructor(name) { this.name = name } + attack() { return `${this.name} attacks!` } + } + + // Classes ARE functions! + console.log(typeof Player) // "function" + + // Methods are on the prototype, not the instance + console.log(Player.prototype.attack) // [Function: attack] + ``` + + This is why JavaScript has quirks like `this` binding issues that don't exist in true class-based languages. + </Accordion> + + <Accordion title="Misconception: 'Factory functions are less powerful than classes'"> + **Reality:** Factory functions can do everything classes can, plus more: + + - **True privacy** via closures (before `#` existed) + - **No `this` binding issues** when using closures + - **Return different types** based on input + - **No `new` keyword** to forget + + ```javascript + // Factory can return different types! + function createShape(type) { + if (type === 'circle') return { radius: 10, area() { /*...*/ } } + if (type === 'square') return { side: 10, area() { /*...*/ } } + } + + // Classes always return instances of that class + ``` + + The trade-off is memory efficiency (classes share methods via prototype). + </Accordion> + + <Accordion title="Misconception: 'Private fields (#) and _underscore are the same thing'"> + **Reality:** They're completely different: + + | Aspect | `_underscore` | `#privateField` | + |--------|---------------|-----------------| + | Accessibility | Fully public | Truly private | + | Convention only? | Yes | No, enforced | + | Error on access | No error | SyntaxError | + + ```javascript + class Account { + _balance = 100 // Accessible! Just a convention + #pin = 1234 // Truly private + } + + const acc = new Account() + console.log(acc._balance) // 100 — works! + // console.log(acc.#pin) // SyntaxError! + ``` + </Accordion> + + <Accordion title="Misconception: 'You should always use classes because they're the modern way'"> + **Reality:** Classes were added in ES6 (2015), but that doesn't mean they're always better. The JavaScript community has moved **toward** functions in many cases: + + - **React:** Moved from class components to function components with hooks + - **Functional programming:** Favors factory functions and composition + - **Simplicity:** Factory functions have fewer footguns (`this`, `new`) + + **Use classes when:** You need `instanceof`, clear hierarchies, or OOP familiarity. + + **Use factories when:** You need composition, true privacy, or functional style. + </Accordion> +</AccordionGroup> + +--- + +## How Does Inheritance Work in JavaScript? + +### Class Inheritance with [`extends`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/extends) + +Use `extends` to create a class that inherits from another: + +```javascript +// Base class (parent) +class Character { + constructor(name, health) { + this.name = name; + this.health = health; + } + + attack() { + return `${this.name} attacks!`; + } + + takeDamage(amount) { + this.health -= amount; + return `${this.name} has ${this.health} HP left.`; + } + + isAlive() { + return this.health > 0; + } +} + +// Derived class (child) +class Warrior extends Character { + constructor(name) { + super(name, 150); // Call parent constructor + this.armor = 20; // Add new property + } + + // Override parent method + takeDamage(amount) { + const reduced = Math.max(0, amount - this.armor); + return super.takeDamage(reduced); // Call parent method + } + + // New method only for Warriors + shieldBash() { + return `${this.name} bashes with shield for ${this.armor} damage!`; + } +} + +// Another derived class +class Mage extends Character { + constructor(name) { + super(name, 80); // Mages have less health + this.mana = 100; + } + + // Override with different behavior + attack() { + if (this.mana >= 10) { + this.mana -= 10; + return `${this.name} casts fireball for 50 damage! (Mana: ${this.mana})`; + } + return `${this.name} is out of mana! Basic attack for 5 damage.`; + } + + meditate() { + this.mana = Math.min(100, this.mana + 30); + return `${this.name} meditates. Mana: ${this.mana}`; + } +} + +// Usage +const conan = new Warrior("Conan"); +const gandalf = new Mage("Gandalf"); + +console.log(conan.attack()); // "Conan attacks!" +console.log(conan.takeDamage(30)); // "Conan has 140 HP left." (reduced by armor) +console.log(conan.shieldBash()); // "Conan bashes with shield for 20 damage!" + +console.log(gandalf.attack()); // "Gandalf casts fireball for 50 damage! (Mana: 90)" +console.log(gandalf.meditate()); // "Gandalf meditates. Mana: 100" + +// instanceof works through the chain +console.log(conan instanceof Warrior); // true +console.log(conan instanceof Character); // true +console.log(gandalf instanceof Mage); // true +console.log(gandalf instanceof Warrior); // false +``` + +### The [`super`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/super) Keyword + +`super` does two things: + +1. **In constructor:** Calls the parent's constructor (`super(...)`) +2. **In methods:** Accesses parent's methods (`super.method()`) + +```javascript +class Animal { + constructor(name) { + this.name = name; + } + + speak() { + return `${this.name} makes a sound.`; + } +} + +class Dog extends Animal { + constructor(name, breed) { + // MUST call super() before using 'this' in derived class + super(name); // Calls Animal's constructor + this.breed = breed; + } + + speak() { + // Call parent method and add to it + const parentSays = super.speak(); + return `${parentSays} Specifically: Woof!`; + } +} + +const rex = new Dog("Rex", "German Shepherd"); +console.log(rex.speak()); +// "Rex makes a sound. Specifically: Woof!" +``` + +<Warning> +**In a derived class constructor, you MUST call `super()` before using `this`.** JavaScript needs to initialize the parent part of the object first. + +```javascript +class Child extends Parent { + constructor(name) { + // this.name = name; // ERROR! Can't use 'this' yet + super(); // Must call super first + this.name = name; // Now 'this' is available + } +} +``` +</Warning> + +### The Problem with Deep Inheritance + +Inheritance can become problematic with deep hierarchies: + +```javascript +// The "Gorilla-Banana Problem" +class Animal { } +class Mammal extends Animal { } +class Primate extends Mammal { } +class Ape extends Primate { } +class Gorilla extends Ape { } + +// You wanted a banana, but you got the whole jungle! +// - Deep chains are hard to understand +// - Changes to parent classes can break children +// - Tight coupling between classes +``` + +### Factory Composition — A Flexible Alternative + +Instead of inheritance ("is-a"), use composition ("has-a"): + +```javascript +// Define behaviors as small, focused functions +const canWalk = (state) => ({ + walk() { + state.position += state.speed; + return `${state.name} walks to position ${state.position}`; + } +}); + +const canSwim = (state) => ({ + swim() { + state.position += state.speed * 1.5; + return `${state.name} swims to position ${state.position}`; + } +}); + +const canFly = (state) => ({ + fly() { + state.position += state.speed * 3; + return `${state.name} flies to position ${state.position}`; + } +}); + +const canSpeak = (state) => ({ + speak(message) { + return `${state.name} says: "${message}"`; + } +}); + +// Compose characters by mixing behaviors +function createDuck(name) { + const state = { name, position: 0, speed: 2 }; + + return { + name: state.name, + ...canWalk(state), + ...canSwim(state), + ...canFly(state), + ...canSpeak(state), + getPosition: () => state.position + }; +} + +function createPenguin(name) { + const state = { name, position: 0, speed: 1 }; + + return { + name: state.name, + ...canWalk(state), + ...canSwim(state), + // No canFly! Penguins can't fly + ...canSpeak(state), + getPosition: () => state.position + }; +} + +function createFish(name) { + const state = { name, position: 0, speed: 4 }; + + return { + name: state.name, + ...canSwim(state), + // Fish can only swim + getPosition: () => state.position + }; +} + +// Usage +const donald = createDuck("Donald"); +donald.walk(); // "Donald walks to position 2" +donald.swim(); // "Donald swims to position 5" +donald.fly(); // "Donald flies to position 11" +donald.speak("Quack!"); // 'Donald says: "Quack!"' + +const tux = createPenguin("Tux"); +tux.walk(); // Works +tux.swim(); // Works +// tux.fly(); // TypeError: tux.fly is not a function + +const nemo = createFish("Nemo"); +nemo.swim(); // Works +// nemo.walk(); // TypeError: nemo.walk is not a function +// nemo.fly(); // TypeError: nemo.fly is not a function +``` + +### Inheritance vs Composition + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ INHERITANCE (is-a) │ +│ │ +│ Animal Problem: What about flying fish? │ +│ │ What about penguins that can't fly? │ +│ ├── Bird (can fly) What about bats (mammals that fly)? │ +│ │ └── Penguin ??? │ +│ ├── Fish (can swim) You end up with awkward hierarchies │ +│ │ └── FlyingFish ??? or lots of override methods. │ +│ └── Mammal │ +│ └── Bat ??? │ +└─────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────┐ +│ COMPOSITION (has-a) │ +│ │ +│ Behaviors: Characters: │ +│ ┌─────────┐ ┌───────────────────────────────────────┐ │ +│ │ canWalk │─────────│ Duck = canWalk + canSwim + canFly │ │ +│ └─────────┘ │ Penguin = canWalk + canSwim │ │ +│ ┌─────────┐ │ Fish = canSwim │ │ +│ │ canSwim │─────────│ FlyingFish = canSwim + canFly │ │ +│ └─────────┘ │ Bat = canWalk + canFly │ │ +│ ┌─────────┐ └───────────────────────────────────────┘ │ +│ │ canFly │ │ +│ └─────────┘ Mix and match any combination! │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +| Aspect | Inheritance | Composition | +|--------|-------------|-------------| +| Relationship | "is-a" (Dog is an Animal) | "has-a" (Duck has flying ability) | +| Flexibility | Rigid hierarchy | Mix and match behaviors | +| Reuse | Through parent chain | Through behavior functions | +| Coupling | Tight (child depends on parent) | Loose (behaviors are independent) | +| Testing | Harder (need parent context) | Easier (test behaviors in isolation) | +| Best for | Clear hierarchies, `instanceof` needed | Flexible combinations, multiple behaviors | + +--- + +## Factory vs Class — Which Should You Use? + +### Side-by-Side Comparison + +| Feature | Factory Function | ES6 Class | +|---------|-----------------|-----------| +| **Syntax** | Regular function | `class` keyword | +| **`new` keyword** | Not needed | Required | +| **`instanceof`** | Doesn't work | Works | +| **True privacy** | Closures | Private fields (#) | +| **Memory efficiency** | Each instance has own methods | Methods shared via prototype | +| **`this` binding** | Can avoid `this` with closures | Must be careful with `this` | +| **Inheritance** | Composition (flexible) | `extends` (hierarchical) | +| **Familiarity** | Functional style | OOP style (familiar to Java/C# devs) | + +### When to Use Factory Functions + +<CardGroup cols={2}> + <Card title="Need true privacy" icon="lock"> + Closure-based privacy can't be circumvented + </Card> + <Card title="No instanceof needed" icon="ban"> + You don't need to check object types + </Card> + <Card title="Composition over inheritance" icon="puzzle-piece"> + Mix and match behaviors flexibly + </Card> + <Card title="Functional programming style" icon="code"> + Fits well with functional patterns + </Card> +</CardGroup> + +### When to Use Classes + +<CardGroup cols={2}> + <Card title="Need instanceof" icon="check"> + Type checking at runtime + </Card> + <Card title="Clear hierarchies" icon="sitemap"> + When "is-a" relationships make sense + </Card> + <Card title="Team familiarity" icon="users"> + Team knows OOP from other languages + </Card> + <Card title="Framework requirements" icon="cubes"> + React components, Angular services, etc. + </Card> +</CardGroup> + +### Decision Guide + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ WHICH SHOULD I USE? │ +│ │ +│ Do you need instanceof checks? │ +│ YES ──► Use Class │ +│ NO ──▼ │ +│ │ +│ Do you need a clear inheritance hierarchy? │ +│ YES ──► Use Class with extends │ +│ NO ──▼ │ +│ │ +│ Do you need to mix multiple behaviors? │ +│ YES ──► Use Factory with composition │ +│ NO ──▼ │ +│ │ +│ Do you need truly private data? │ +│ YES ──► Either works (Factory closures OR Class with #) │ +│ NO ──▼ │ +│ │ +│ Is your team familiar with OOP? │ +│ YES ──► Use Class (more familiar syntax) │ +│ NO ──► Use Factory (simpler mental model) │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +### Real-World Examples + +**React Components (Classes → Functions)** +```javascript +// Old: Class components +class Button extends React.Component { + render() { + return <button>{this.props.label}</button>; + } +} + +// Modern: Function components (like factories) +function Button({ label }) { + return <button>{label}</button>; +} +``` + +**Game Entities (Classes for hierarchy)** +```javascript +class Entity { } +class Character extends Entity { } +class Player extends Character { } +class NPC extends Character { } +``` + +**Utility Objects (Factories for flexibility)** +```javascript +const logger = createLogger({ level: 'debug', prefix: '[App]' }); +const cache = createCache({ maxSize: 100, ttl: 3600 }); +``` + +--- + +## Key Takeaways + +<Info> +**The key things to remember:** + +1. **Factory functions** are regular functions that return objects — simple and flexible + +2. **Constructor functions** are used with `new` to create instances — the traditional approach + +3. **ES6 classes** are syntactic sugar over constructors — cleaner syntax, same behavior + +4. **The `new` keyword** creates an object, links its prototype, runs the constructor, and returns the result + +5. **Prototype methods** are shared by all instances — saves memory + +6. **Private fields (#)** provide true privacy in classes — can't be accessed from outside + +7. **Closures** provide true privacy in factories — variables trapped in function scope + +8. **Static methods** belong to the class itself, not instances — use for utilities and factory methods + +9. **Inheritance (`extends`)** creates "is-a" relationships — use for clear hierarchies + +10. **Composition** creates "has-a" relationships — more flexible than inheritance + +11. **Use classes** when you need `instanceof`, clear hierarchies, or team familiarity + +12. **Use factories** when you need composition, true privacy, or functional style +</Info> + +--- + +## Test Your Knowledge + +<AccordionGroup> + <Accordion title="Question 1: What are the 4 steps the new keyword performs?"> + **Answer:** + + When you call `new Constructor(args)`: + + 1. **Create** a new empty object (`{}`) + 2. **Link** the object's prototype to `Constructor.prototype` + 3. **Execute** the constructor with `this` bound to the new object + 4. **Return** the object (unless constructor returns a different object) + + ```javascript + function myNew(Constructor, ...args) { + const obj = Object.create(Constructor.prototype); // Steps 1-2 + const result = Constructor.apply(obj, args); // Step 3 + return (typeof result === 'object' && result !== null) ? result : obj; // Step 4 + } + ``` + </Accordion> + + <Accordion title="Question 2: What's the difference between instance methods and prototype methods?"> + **Answer:** + + **Instance methods** are defined in the constructor — each instance gets its own copy: + ```javascript + function Player(name) { + this.attack = function() { }; // Each player has own attack function + } + ``` + + **Prototype methods** are shared by all instances — more memory efficient: + ```javascript + Player.prototype.attack = function() { }; // All players share one function + ``` + + In ES6 classes, methods defined in the class body are automatically prototype methods: + ```javascript + class Player { + attack() { } // This goes on Player.prototype + } + ``` + </Accordion> + + <Accordion title="Question 3: How do private fields (#) differ from closure-based privacy?"> + **Answer:** + + | Aspect | Private Fields (#) | Closures | + |--------|-------------------|----------| + | Syntax | `this.#field` | `let variable` in factory | + | Error on access | SyntaxError | Returns `undefined` | + | Memory | Efficient (shared methods) | Each instance has own methods | + | `instanceof` | Works | Doesn't work | + + ```javascript + // Private Fields + class Wallet { + #balance = 0; + getBalance() { return this.#balance; } + } + // w.#balance throws SyntaxError + + // Closures + function createWallet() { + let balance = 0; + return { getBalance() { return balance; } }; + } + // w.balance returns undefined + ``` + </Accordion> + + <Accordion title="Question 4: What does super() do and when must you call it?"> + **Answer:** + + `super()` calls the parent class's constructor. You **must** call it in a derived class constructor **before** using `this`. + + ```javascript + class Animal { + constructor(name) { + this.name = name; + } + } + + class Dog extends Animal { + constructor(name, breed) { + // this.breed = breed; // ERROR! Can't use 'this' yet + super(name); // Call parent constructor first + this.breed = breed; // Now 'this' is available + } + } + ``` + + `super.method()` calls a parent's method from within an overriding method. + </Accordion> + + <Accordion title="Question 5: When would you use composition over inheritance?"> + **Answer:** + + Use **composition** when: + - You need to mix behaviors from multiple sources (a flying fish, a swimming bird) + - The "is-a" relationship doesn't make sense + - You want loose coupling between components + - You need flexibility to change behaviors at runtime + + Use **inheritance** when: + - There's a clear "is-a" hierarchy (Dog is an Animal) + - You need `instanceof` checks + - You want to share implementation, not just interface + + **Rule of thumb:** "Favor composition over inheritance" — composition is more flexible. + </Accordion> + + <Accordion title="Question 6: Why are ES6 classes called 'syntactic sugar'?"> + **Answer:** + + Classes are called "syntactic sugar" because they don't add new functionality — they just provide a cleaner syntax for constructor functions and prototypes. + + ```javascript + // This class... + class Player { + constructor(name) { this.name = name; } + attack() { return `${this.name} attacks!`; } + } + + // ...is equivalent to: + function Player(name) { this.name = name; } + Player.prototype.attack = function() { return `${this.name} attacks!`; }; + + // Both create the same result: + typeof Player === 'function' // true for both + ``` + + The class syntax makes the code easier to read and write, but under the hood, JavaScript is still using prototypes. + </Accordion> +</AccordionGroup> + +--- + +## Related Concepts + +<CardGroup cols={2}> + <Card title="Object Creation & Prototypes" icon="link" href="/concepts/object-creation-prototypes"> + Deep dive into JavaScript's prototype chain, Object.create(), and how the new keyword works under the hood + </Card> + <Card title="this, call, apply, bind" icon="hand-pointer" href="/concepts/this-call-apply-bind"> + Understanding this binding in different contexts + </Card> + <Card title="Inheritance and Polymorphism" icon="sitemap" href="/concepts/inheritance-polymorphism"> + Advanced inheritance patterns and polymorphism in JavaScript + </Card> + <Card title="Design Patterns" icon="compass" href="/concepts/design-patterns"> + Common patterns including Factory, Singleton, and more + </Card> +</CardGroup> + +--- + +## Reference + +<CardGroup cols={2}> + <Card title="Classes — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes"> + Official MDN documentation on ES6 classes + </Card> + <Card title="Private class features — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_class_fields"> + Documentation on private fields and methods + </Card> + <Card title="new operator — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new"> + How the new keyword works + </Card> + <Card title="Object.create() — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create"> + Creating objects with specific prototypes + </Card> +</CardGroup> + +--- + +## Articles + +<CardGroup cols={2}> + <Card title="How To Use Classes in JavaScript" icon="newspaper" href="https://www.digitalocean.com/community/tutorials/understanding-classes-in-javascript"> + Tania builds a Character class step by step, adding features one at a time. Great if you want to follow along and type the code yourself. + </Card> + <Card title="JavaScript Classes — Under The Hood" icon="newspaper" href="https://talkingtech.io/javascript-classes-under-the-hood/"> + Shows the ES5 equivalent of every ES6 class feature side by side. Read this to understand what JavaScript is really doing when you write a class. + </Card> + <Card title="Factory Functions in JavaScript" icon="newspaper" href="https://atendesigngroup.com/blog/factory-functions-javascript"> + A classic introduction to factory functions using a Car example. Shows the self-pattern for avoiding `this` issues and private variables with closures. + </Card> + <Card title="Class vs Factory function" icon="newspaper" href="https://medium.freecodecamp.org/class-vs-factory-function-exploring-the-way-forward-73258b6a8d15"> + Cristi Salcescu's comparison of both approaches with pros, cons, and when to use each. + </Card> + <Card title="Composition vs Inheritance" icon="newspaper" href="https://ui.dev/javascript-inheritance-vs-composition/"> + Uses a game character example to show how composition avoids the problems of deep inheritance. Includes the mixin pattern for adding behaviors. + </Card> + <Card title="Understanding super in JavaScript" icon="newspaper" href="https://jordankasper.com/understanding-super-in-javascript"> + Explains when and why you need super() with clear error examples. Covers the "must call super before this" rule that trips up beginners. + </Card> +</CardGroup> + +--- + +## Videos + +<CardGroup cols={2}> + <Card title="JavaScript Factory Functions" icon="video" href="https://www.youtube.com/watch?v=jpegXpQpb3o"> + Mosh builds a circle factory from scratch in under 10 minutes. Good starting point if you've never seen factories before. + </Card> + <Card title="Factory Functions in JavaScript" icon="video" href="https://www.youtube.com/watch?v=ImwrezYhw4w"> + MPJ's signature conversational style makes factories feel approachable. Includes the "why not just use classes?" discussion. + </Card> + <Card title="Composition over Inheritance" icon="video" href="https://www.youtube.com/watch?v=wfMtDGfHWpA"> + Fun Fun Function explains why composition is often better than inheritance with the "Gorilla-Banana" problem. + </Card> + <Card title="JavaScript Classes Tutorial" icon="video" href="https://www.youtube.com/watch?v=2ZphE5HcQPQ"> + Traversy covers classes from basic syntax to private fields in one video. Watch at 1.5x speed for a quick refresher. + </Card> +</CardGroup> diff --git a/docs/concepts/generators-iterators.mdx b/docs/concepts/generators-iterators.mdx new file mode 100644 index 00000000..c11e38aa --- /dev/null +++ b/docs/concepts/generators-iterators.mdx @@ -0,0 +1,1489 @@ +--- +title: "Generators & Iterators: Pausable Functions in JavaScript" +sidebarTitle: "Generators & Iterators: Pausable Functions" +description: "Learn JavaScript generators and iterators. Understand yield, the iteration protocol, lazy evaluation, infinite sequences, and async generators with for await...of." +--- + +What if a function could pause mid-execution, return a value, and then resume right where it left off? What if you could create a sequence of values that are computed only when you ask for them — not all at once? + +```javascript +// This function can PAUSE and RESUME +function* countToThree() { + yield 1 // Pause here, return 1 + yield 2 // Resume, pause here, return 2 + yield 3 // Resume, pause here, return 3 +} + +const counter = countToThree() + +console.log(counter.next().value) // 1 +console.log(counter.next().value) // 2 +console.log(counter.next().value) // 3 +``` + +This is the power of **[generators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator)**. These are functions that can pause with `yield` and pick up where they left off. Combined with **[iterators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols)** (objects that define how to step through a sequence), they open up patterns like lazy evaluation, infinite sequences, and clean data pipelines. + +<Info> +**What you'll learn in this guide:** +- What iterators are and how the iteration protocol works +- Generator functions with `function*` and `yield` (they're lazier than you think) +- The difference between `yield` and `return` (it trips people up!) +- How to make any object iterable with `Symbol.iterator` +- Lazy evaluation — why generators are so memory-efficient +- Practical patterns: pagination, ID generation, state machines +- Async generators and `for await...of` for streaming data +</Info> + +<Warning> +**Prerequisites:** This guide assumes you're comfortable with [closures](/concepts/scope-and-closures) and [higher-order functions](/concepts/higher-order-functions). If those concepts are new to you, read those guides first! +</Warning> + +--- + +## What is an Iterator? + +Before getting into generators, we need to cover **iterators**, the foundation that makes generators work. + +An **[iterator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterator_protocol)** is an object that defines a sequence and provides a way to access values one at a time. It must have a `.next()` method that returns an object with two properties: + +- `value` — the next value in the sequence +- `done` — `true` if the sequence is finished, `false` otherwise + +```javascript +// Creating an iterator manually +function createCounterIterator(max) { + let count = 0 + + return { + next() { + if (count < max) { + return { value: count++, done: false } + } else { + return { value: undefined, done: true } + } + } + } +} + +const counter = createCounterIterator(3) + +console.log(counter.next()) // { value: 0, done: false } +console.log(counter.next()) // { value: 1, done: false } +console.log(counter.next()) // { value: 2, done: false } +console.log(counter.next()) // { value: undefined, done: true } +``` + +### Why Iterators? + +Why not just use an array? Two reasons: + +1. **Lazy evaluation** — Values are computed only when you ask for them, not upfront +2. **Memory efficiency** — You don't need to hold the entire sequence in memory + +Say you need to process a million records. With an array, you'd load all million into memory. With an iterator, you process one at a time. Memory stays flat. + +### Built-in Iterables + +Many JavaScript built-ins are already **iterable** (they have iterators built in): + +| Type | Example | What it iterates over | +|------|---------|----------------------| +| **Array** | `[1, 2, 3]` | Each element | +| **String** | `"hello"` | Each character | +| **Map** | `new Map([['a', 1]])` | Each `[key, value]` pair | +| **Set** | `new Set([1, 2, 3])` | Each unique value | +| **arguments** | `arguments` object | Each argument passed to a function | +| **NodeList** | `document.querySelectorAll('div')` | Each DOM node | + +You can access their iterator using `Symbol.iterator`: + +```javascript +const arr = [10, 20, 30] +const iterator = arr[Symbol.iterator]() + +console.log(iterator.next()) // { value: 10, done: false } +console.log(iterator.next()) // { value: 20, done: false } +console.log(iterator.next()) // { value: 30, done: false } +console.log(iterator.next()) // { value: undefined, done: true } +``` + +<Note> +**`for...of` uses iterators under the hood.** When you write `for (const item of array)`, JavaScript is actually calling the iterator's `.next()` method repeatedly until `done` is `true`. +</Note> + +--- + +## The Vending Machine Analogy + +Generators click when you have the right mental picture. Think of them like a **vending machine**: + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ GENERATOR AS A VENDING MACHINE │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ YOU VENDING MACHINE │ +│ (caller) (generator) │ +│ │ +│ ┌─────────┐ ┌─────────────────┐ │ +│ │ │ │ ┌───────────┐ │ │ +│ │ "I'll │ ──── Press button ─────────► │ │ Snack A │ │ │ +│ │ have │ (call .next()) │ ├───────────┤ │ │ +│ │ one" │ │ │ Snack B │ │ │ +│ │ │ ◄─── Dispense one item ───── │ ├───────────┤ │ │ +│ │ │ (yield value) │ │ Snack C │ │ │ +│ │ │ │ └───────────┘ │ │ +│ │ │ * Machine PAUSES * │ │ │ +│ │ │ * Waits for next * │ [ PAUSED ] │ │ +│ │ │ * button press * │ │ │ +│ └─────────┘ └─────────────────┘ │ +│ │ +│ KEY INSIGHT: The machine remembers where it stopped! │ +│ When you press the button again, it gives you the NEXT item, │ +│ not the first one again. │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +Here's how this maps to generator concepts: + +| Vending Machine | Generator | +|-----------------|-----------| +| Press the button | Call `.next()` | +| Machine dispenses one item | `yield` returns a value | +| Machine pauses, waits | Generator pauses at `yield` | +| Press button again | Call `.next()` again | +| Machine remembers position | Generator remembers its state | +| Machine is empty | `done: true` | + +A generator works the same way: one value at a time, pausing between each. + +--- + +## What is a Generator? + +A **[generator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator)** is a function that can stop mid-execution, hand you a value, and pick up where it left off later. You create one using `function*` (note the asterisk) and pause it with the `yield` keyword. + +```javascript +// The asterisk (*) makes this a generator function +function* myGenerator() { + console.log('Starting...') + yield 'First value' + + console.log('Resuming...') + yield 'Second value' + + console.log('Finishing...') + return 'Done!' +} +``` + +When you call a generator function, the code inside doesn't run yet. You just get back a **generator object** (which is an iterator): + +```javascript +const gen = myGenerator() // Nothing logs yet! + +console.log(gen) // Object [Generator] {} +``` + +The code only runs when you call `.next()`: + +```javascript +const gen = myGenerator() + +// First .next() — runs until first yield +console.log(gen.next()) +// Logs: "Starting..." +// Returns: { value: 'First value', done: false } + +// Second .next() — resumes and runs until second yield +console.log(gen.next()) +// Logs: "Resuming..." +// Returns: { value: 'Second value', done: false } + +// Third .next() — resumes and runs to the end +console.log(gen.next()) +// Logs: "Finishing..." +// Returns: { value: 'Done!', done: true } + +// Fourth .next() — generator is exhausted +console.log(gen.next()) +// Returns: { value: undefined, done: true } +``` + +### Generators are Iterators + +Because generator objects follow the iterator protocol, you can use them with `for...of`: + +```javascript +function* colors() { + yield 'red' + yield 'green' + yield 'blue' +} + +for (const color of colors()) { + console.log(color) +} +// Output: +// red +// green +// blue +``` + +You can also spread them into arrays: + +```javascript +function* numbers() { + yield 1 + yield 2 + yield 3 +} + +const arr = [...numbers()] +console.log(arr) // [1, 2, 3] +``` + +<CardGroup cols={2}> + <Card title="Generator — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator"> + Official MDN documentation for Generator objects + </Card> + <Card title="function* — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*"> + Documentation for the generator function syntax + </Card> +</CardGroup> + +--- + +## The `yield` Keyword Deep Dive + +`yield` is what makes generators tick. It pauses the function and sends a value back to the caller. When you call `.next()` again, execution picks up right after the `yield`. + +### Basic `yield` + +```javascript +function* countdown() { + yield 3 + yield 2 + yield 1 + yield 'Liftoff!' +} + +const rocket = countdown() + +console.log(rocket.next().value) // 3 +console.log(rocket.next().value) // 2 +console.log(rocket.next().value) // 1 +console.log(rocket.next().value) // "Liftoff!" +``` + +### `yield` vs `return` + +Both `yield` and `return` can return values, but they behave very differently: + +| `yield` | `return` | +|---------|----------| +| Pauses the generator | Ends the generator | +| `done: false` | `done: true` | +| Can have multiple | Only one matters | +| Value accessible in `for...of` | Value NOT accessible in `for...of` | + +```javascript +function* example() { + yield 'A' // Pauses, done: false + yield 'B' // Pauses, done: false + return 'C' // Ends, done: true +} + +// With for...of — return value is ignored! +for (const val of example()) { + console.log(val) +} +// Output: A, B (no C!) + +// With .next() — you can see the return value +const gen = example() +console.log(gen.next()) // { value: 'A', done: false } +console.log(gen.next()) // { value: 'B', done: false } +console.log(gen.next()) // { value: 'C', done: true } +``` + +<Warning> +**Common gotcha:** The value from `return` is not included when iterating with `for...of`, spread syntax, or `Array.from()`. Use `yield` for all values you want to iterate over. +</Warning> + +### `yield*` — Delegating to Other Iterables + +When you want to pass through all values from another iterable, use `yield*`: + +```javascript +function* inner() { + yield 'a' + yield 'b' +} + +function* outer() { + yield 1 + yield* inner() // Delegates to inner generator + yield 2 +} + +console.log([...outer()]) // [1, 'a', 'b', 2] +``` + +`yield*` shines when flattening nested structures: + +```javascript +function* flatten(arr) { + for (const item of arr) { + if (Array.isArray(item)) { + yield* flatten(item) // Recursively delegate + } else { + yield item + } + } +} + +const nested = [1, [2, 3, [4, 5]], 6] +console.log([...flatten(nested)]) // [1, 2, 3, 4, 5, 6] +``` + +### Passing Values INTO Generators + +You can also send values *into* a generator by passing them to `.next(value)`. The value becomes the result of the `yield` expression inside the generator: + +```javascript +function* conversation() { + const name = yield 'What is your name?' + const color = yield `Hello, ${name}! What's your favorite color?` + yield `${color} is a great color, ${name}!` +} + +const chat = conversation() + +// First .next() — no value needed, just starts the generator +console.log(chat.next().value) +// "What is your name?" + +// Second .next() — pass in the answer +console.log(chat.next('Alice').value) +// "Hello, Alice! What's your favorite color?" + +// Third .next() — pass in another answer +console.log(chat.next('Blue').value) +// "Blue is a great color, Alice!" +``` + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ DATA FLOW WITH yield │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ CALLER GENERATOR │ +│ │ +│ .next() ─────────────────────► starts execution │ +│ ◄───────────────────── yield 'question' │ +│ │ +│ .next('Alice') ─────────────────────► const name = 'Alice' │ +│ ◄───────────────────── yield 'Hello Alice' │ +│ │ +│ .next('Blue') ─────────────────────► const color = 'Blue' │ +│ ◄───────────────────── yield 'Blue is great' │ +│ │ +│ The value passed to .next() becomes the RESULT of the yield │ +│ expression inside the generator. │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +<Note> +**Why no value in the first `.next()`?** The first call starts the generator and runs until the first `yield`. There's no `yield` waiting to receive a value yet, so anything you pass gets ignored. +</Note> + +### Generator Control Methods: `.return()` and `.throw()` + +Beyond `.next()`, generators have two more control methods that give you full control over execution. + +#### Early Termination with `.return()` + +The `.return(value)` method ends the generator immediately and returns the specified value: + +```javascript +function* countdown() { + yield 3 + yield 2 + yield 1 + yield 'Liftoff!' +} + +const rocket = countdown() + +console.log(rocket.next()) // { value: 3, done: false } +console.log(rocket.return('Aborted')) // { value: 'Aborted', done: true } +console.log(rocket.next()) // { value: undefined, done: true } +// Generator is now closed — subsequent .next() calls return done: true +``` + +This is useful for cleanup or when you need to stop iteration early. + +#### Error Injection with `.throw()` + +The `.throw(error)` method throws an exception at the current `yield` point. If the generator has a `try/catch`, it can handle the error: + +```javascript +function* resilientGenerator() { + try { + yield 'A' + yield 'B' + yield 'C' + } catch (e) { + yield `Caught: ${e.message}` + } + yield 'Done' +} + +const gen = resilientGenerator() + +console.log(gen.next().value) // "A" +console.log(gen.throw(new Error('Oops!')).value) // "Caught: Oops!" +console.log(gen.next().value) // "Done" +``` + +If there's no `try/catch`, the error propagates out: + +```javascript +function* fragileGenerator() { + yield 'A' + yield 'B' // Error thrown here if we call .throw() after first yield +} + +const gen = fragileGenerator() +gen.next() // { value: 'A', done: false } + +try { + gen.throw(new Error('Boom!')) +} catch (e) { + console.log(e.message) // "Boom!" +} +``` + +<Tip> +These methods complete the generator's interface. While `.next()` is used most often, `.return()` and `.throw()` give you full control over generator execution — useful for resource cleanup and error handling in complex workflows. +</Tip> + +--- + +## The Iteration Protocol (`Symbol.iterator`) + +Now for the fun part: making your own objects work with `for...of`. An object is **iterable** if it has a `[Symbol.iterator]` method that returns an iterator. + +### Making a Custom Object Iterable + +```javascript +const myCollection = { + items: ['apple', 'banana', 'cherry'], + + // This makes the object iterable + [Symbol.iterator]() { + let index = 0 + const items = this.items + + return { + next() { + if (index < items.length) { + return { value: items[index++], done: false } + } else { + return { value: undefined, done: true } + } + } + } + } +} + +// Now we can use for...of! +for (const item of myCollection) { + console.log(item) +} +// Output: apple, banana, cherry + +// And spread syntax! +console.log([...myCollection]) // ['apple', 'banana', 'cherry'] +``` + +### Using Generators to Simplify Iterators + +All that manual iterator code? Generators cut it down to almost nothing: + +```javascript +const myCollection = { + items: ['apple', 'banana', 'cherry'], + + // Generator as the Symbol.iterator method + *[Symbol.iterator]() { + for (const item of this.items) { + yield item + } + } +} + +for (const item of myCollection) { + console.log(item) +} +// Output: apple, banana, cherry +``` + +### Example: Creating an Iterable Range + +Here's a `Range` class you can loop over with `for...of`: + +```javascript +class Range { + constructor(start, end, step = 1) { + this.start = start + this.end = end + this.step = step + } + + // Generator makes this easy! + *[Symbol.iterator]() { + for (let i = this.start; i <= this.end; i += this.step) { + yield i + } + } +} + +const oneToFive = new Range(1, 5) +console.log([...oneToFive]) // [1, 2, 3, 4, 5] + +const evens = new Range(0, 10, 2) +console.log([...evens]) // [0, 2, 4, 6, 8, 10] + +// Works with for...of +for (const n of new Range(1, 3)) { + console.log(n) // 1, 2, 3 +} +``` + +### What `for...of` Really Does + +When you write a `for...of` loop, JavaScript does this behind the scenes: + +<Steps> + <Step title="Get the iterator"> + JavaScript calls `iterable[Symbol.iterator]()` to get an iterator object. + </Step> + <Step title="Call .next()"> + The loop calls `iterator.next()` to get the first `{ value, done }` result. + </Step> + <Step title="Check if done"> + If `done` is `false`, the `value` goes into your loop variable. + </Step> + <Step title="Repeat until done"> + Steps 2-3 repeat until `done` is `true`, then the loop exits. + </Step> +</Steps> + +Here's what that looks like in code: + +```javascript +// This: +for (const item of iterable) { + console.log(item) +} + +// Is equivalent to this: +const iterator = iterable[Symbol.iterator]() +let result = iterator.next() + +while (!result.done) { + const item = result.value + console.log(item) + result = iterator.next() +} +``` + +<Tip> +**When to make something iterable:** If your object represents a collection or sequence of values, making it iterable allows it to work with `for...of`, spread syntax, `Array.from()`, destructuring, and more. +</Tip> + +--- + +## Lazy Evaluation & Infinite Sequences + +The killer feature of generators is **lazy evaluation**. Values are computed only when you ask for them, not ahead of time. + +### Memory Efficiency + +Compare these two approaches for creating a range of numbers: + +```javascript +// Eager evaluation — creates entire array in memory +function rangeArray(start, end) { + const result = [] + for (let i = start; i <= end; i++) { + result.push(i) + } + return result +} + +// Lazy evaluation — computes values on demand +function* rangeGenerator(start, end) { + for (let i = start; i <= end; i++) { + yield i + } +} + +// For small ranges, both work fine +console.log(rangeArray(1, 5)) // [1, 2, 3, 4, 5] +console.log([...rangeGenerator(1, 5)]) // [1, 2, 3, 4, 5] + +// For large ranges, generators shine +// rangeArray(1, 1000000) — Creates array of 1 million numbers! +// rangeGenerator(1, 1000000) — Creates nothing until you iterate +``` + +### Infinite Sequences + +Because generators are lazy, you can create **infinite sequences**, something impossible with arrays: + +```javascript +// Infinite sequence of natural numbers +function* naturalNumbers() { + let n = 1 + while (true) { // Infinite loop! + yield n++ + } +} + +// This would crash with an array, but generators are lazy +const numbers = naturalNumbers() + +console.log(numbers.next().value) // 1 +console.log(numbers.next().value) // 2 +console.log(numbers.next().value) // 3 +// We can keep going forever... +``` + +### Fibonacci Sequence + +A classic example: the infinite Fibonacci sequence: + +```javascript +function* fibonacci() { + let prev = 0 + let curr = 1 + + while (true) { + yield curr + const next = prev + curr + prev = curr + curr = next + } +} + +const fib = fibonacci() + +console.log(fib.next().value) // 1 +console.log(fib.next().value) // 1 +console.log(fib.next().value) // 2 +console.log(fib.next().value) // 3 +console.log(fib.next().value) // 5 +console.log(fib.next().value) // 8 +``` + +### Taking N Items from an Infinite Generator + +You'll often want to take a limited number of items from an infinite generator: + +```javascript +// Helper function to take N items from any iterable +function* take(n, iterable) { + let count = 0 + for (const item of iterable) { + if (count >= n) return + yield item + count++ + } +} + +// Get first 10 Fibonacci numbers +const firstTenFib = [...take(10, fibonacci())] +console.log(firstTenFib) // [1, 1, 2, 3, 5, 8, 13, 21, 34, 55] + +// Get first 5 natural numbers +const firstFive = [...take(5, naturalNumbers())] +console.log(firstFive) // [1, 2, 3, 4, 5] +``` + +<Warning> +**Be careful with infinite generators!** Never use `[...infiniteGenerator()]` or `for...of` on an infinite generator without a break condition. Your program will hang trying to iterate forever. + +```javascript +// ❌ DANGER — This will hang/crash! +const all = [...naturalNumbers()] // Trying to collect infinite items + +// ✓ SAFE — Use take() or break early +const some = [...take(100, naturalNumbers())] +``` +</Warning> + +--- + +## Common Patterns + +Here are some patterns that make generators worth knowing. + +### Pattern 1: Unique ID Generator + +Generate unique IDs without tracking global state: + +```javascript +function* createIdGenerator(prefix = 'id') { + let id = 1 + while (true) { + yield `${prefix}_${id++}` + } +} + +const userIds = createIdGenerator('user') +const orderIds = createIdGenerator('order') + +console.log(userIds.next().value) // "user_1" +console.log(userIds.next().value) // "user_2" +console.log(orderIds.next().value) // "order_1" +console.log(userIds.next().value) // "user_3" +console.log(orderIds.next().value) // "order_2" +``` + +### Pattern 2: Pagination / Chunking Data + +Process large datasets in manageable chunks: + +```javascript +function* chunk(array, size) { + for (let i = 0; i < array.length; i += size) { + yield array.slice(i, i + size) + } +} + +const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + +for (const batch of chunk(data, 3)) { + console.log('Processing batch:', batch) +} +// Output: +// Processing batch: [1, 2, 3] +// Processing batch: [4, 5, 6] +// Processing batch: [7, 8, 9] +// Processing batch: [10] +``` + +This is great for batch processing, API rate limiting, or breaking up heavy computations: + +```javascript +function* processInBatches(items, batchSize) { + for (const batch of chunk(items, batchSize)) { + // Process each batch + const results = batch.map(item => heavyComputation(item)) + yield results + } +} + +// Process 1000 items in batches of 100 +const allItems = new Array(1000).fill(null).map((_, i) => i) + +for (const batchResults of processInBatches(allItems, 100)) { + console.log(`Processed ${batchResults.length} items`) + // Could add delay here to avoid blocking the main thread +} +``` + +### Pattern 3: Filtering and Transforming Data + +Create composable data pipelines: + +```javascript +function* filter(iterable, predicate) { + for (const item of iterable) { + if (predicate(item)) { + yield item + } + } +} + +function* map(iterable, transform) { + for (const item of iterable) { + yield transform(item) + } +} + +// Compose them together +function* range(start, end) { + for (let i = start; i <= end; i++) { + yield i + } +} + +// Pipeline: numbers 1-10 → filter evens → double them +const result = map( + filter(range(1, 10), n => n % 2 === 0), + n => n * 2 +) + +console.log([...result]) // [4, 8, 12, 16, 20] +``` + +### Pattern 4: Simple State Machine + +Generators naturally model state machines because they remember their position: + +```javascript +function* trafficLight() { + while (true) { + yield 'green' + yield 'yellow' + yield 'red' + } +} + +const light = trafficLight() + +console.log(light.next().value) // "green" +console.log(light.next().value) // "yellow" +console.log(light.next().value) // "red" +console.log(light.next().value) // "green" (cycles back) +console.log(light.next().value) // "yellow" +``` + +A more complex example with different wait times: + +```javascript +function* trafficLightWithDurations() { + while (true) { + yield { color: 'green', duration: 30000 } // 30 seconds + yield { color: 'yellow', duration: 5000 } // 5 seconds + yield { color: 'red', duration: 25000 } // 25 seconds + } +} + +const light = trafficLightWithDurations() + +function changeLight() { + const { color, duration } = light.next().value + console.log(`Light is now ${color} for ${duration / 1000} seconds`) + setTimeout(changeLight, duration) +} + +// changeLight() // Uncomment to run +``` + +### Pattern 5: Tree Traversal + +Generators work great for traversing trees: + +```javascript +function* traverseTree(node) { + yield node.value + + if (node.children) { + for (const child of node.children) { + yield* traverseTree(child) // Recursive delegation + } + } +} + +const tree = { + value: 'root', + children: [ + { + value: 'child1', + children: [ + { value: 'grandchild1' }, + { value: 'grandchild2' } + ] + }, + { + value: 'child2', + children: [ + { value: 'grandchild3' } + ] + } + ] +} + +console.log([...traverseTree(tree)]) +// ['root', 'child1', 'grandchild1', 'grandchild2', 'child2', 'grandchild3'] +``` + +--- + +## Async Generators & `for await...of` + +What about yielding values from async operations like API calls, file reads, that kind of thing? That's what **async generators** are for. + +### The Problem with Regular Generators + +Regular generators are synchronous. If you try to yield a Promise, you get the Promise object itself, not its resolved value: + +```javascript +function* fetchUsers() { + yield fetch('/api/user/1').then(r => r.json()) + yield fetch('/api/user/2').then(r => r.json()) +} + +const gen = fetchUsers() +console.log(gen.next().value) // Promise { <pending> } — not the user! +``` + +### Async Generator Syntax + +An **[async generator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function*)** combines `async` functions with generators. You can `await` inside them, and you iterate with `for await...of`: + +```javascript +async function* fetchUsersAsync() { + const user1 = await fetch('/api/user/1').then(r => r.json()) + yield user1 + + const user2 = await fetch('/api/user/2').then(r => r.json()) + yield user2 +} + +// Use for await...of to consume +async function displayUsers() { + for await (const user of fetchUsersAsync()) { + console.log(user.name) + } +} +``` + +### Practical Example: Paginated API + +Fetch all pages of data from a paginated API: + +```javascript +async function* fetchAllPages(baseUrl) { + let page = 1 + let hasMore = true + + while (hasMore) { + const response = await fetch(`${baseUrl}?page=${page}`) + const data = await response.json() + + yield data.items // Yield this page's items + + hasMore = data.hasNextPage + page++ + } +} + +// Process all pages +async function processAllUsers() { + for await (const pageOfUsers of fetchAllPages('/api/users')) { + console.log(`Processing ${pageOfUsers.length} users...`) + + for (const user of pageOfUsers) { + // Process each user + await saveToDatabase(user) + } + } +} +``` + +### Async Generator vs Promise.all + +When do you reach for an async generator over `Promise.all`? + +```javascript +// Promise.all — All requests in parallel, wait for ALL to complete +async function fetchAllAtOnce(userIds) { + const users = await Promise.all( + userIds.map(id => fetch(`/api/user/${id}`).then(r => r.json())) + ) + return users // Returns all users at once +} + +// Async generator — Process as each completes +async function* fetchOneByOne(userIds) { + for (const id of userIds) { + const user = await fetch(`/api/user/${id}`).then(r => r.json()) + yield user // Yield each user as it's fetched + } +} +``` + +| Approach | Best for | +|----------|----------| +| `Promise.all` | When you need all results before proceeding | +| Async generator | When you want to process results as they arrive | +| Async generator | When fetching everything at once would be too memory-intensive | +| Async generator | When you might want to stop early | + +### Reading Lines from a Stream + +Here's a real pattern for processing a stream line by line: + +```javascript +async function* readLines(reader) { + const decoder = new TextDecoder() + let buffer = '' + + while (true) { + const { done, value } = await reader.read() + + if (done) { + if (buffer) yield buffer // Yield any remaining content + return + } + + buffer += decoder.decode(value, { stream: true }) + const lines = buffer.split('\n') + buffer = lines.pop() // Keep incomplete line in buffer + + for (const line of lines) { + yield line + } + } +} + +// Usage with fetch +async function processLogFile(url) { + const response = await fetch(url) + const reader = response.body.getReader() + + for await (const line of readLines(reader)) { + console.log('Log entry:', line) + } +} +``` + +<CardGroup cols={2}> + <Card title="async function* — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function*"> + Documentation for async generator functions + </Card> + <Card title="for await...of — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of"> + Documentation for async iteration + </Card> +</CardGroup> + +--- + +## Common Mistakes + +<AccordionGroup> + <Accordion title="Mistake 1: Forgetting the asterisk in function*"> + ```javascript + // ❌ WRONG — This is a regular function, not a generator + function myGenerator() { + yield 1 // SyntaxError: Unexpected number + } + + // ✓ CORRECT — Note the asterisk + function* myGenerator() { + yield 1 + } + ``` + + The asterisk can go next to `function` or next to the name — both work: + + ```javascript + function* foo() {} // ✓ + function *foo() {} // ✓ + function * foo() {} // ✓ + ``` + </Accordion> + + <Accordion title="Mistake 2: Expecting generator to run immediately"> + ```javascript + // ❌ WRONG — Nothing happens when you call a generator function + function* greet() { + console.log('Hello!') + yield 'Hi' + } + + greet() // Nothing logged! Returns generator object + + // ✓ CORRECT — You must call .next() or iterate + const gen = greet() + gen.next() // NOW it logs "Hello!" + + // Or use for...of + for (const val of greet()) { + console.log(val) + } + ``` + </Accordion> + + <Accordion title="Mistake 3: Using return instead of yield for iteration values"> + ```javascript + // ❌ WRONG — return value won't appear in for...of + function* letters() { + yield 'a' + yield 'b' + return 'c' // This won't be iterated! + } + + console.log([...letters()]) // ['a', 'b'] — no 'c'! + + // ✓ CORRECT — Use yield for all iteration values + function* letters() { + yield 'a' + yield 'b' + yield 'c' + } + + console.log([...letters()]) // ['a', 'b', 'c'] + ``` + </Accordion> + + <Accordion title="Mistake 4: Reusing an exhausted generator"> + ```javascript + // ❌ WRONG — Generators can only be iterated once + function* nums() { + yield 1 + yield 2 + } + + const gen = nums() + console.log([...gen]) // [1, 2] + console.log([...gen]) // [] — generator is exhausted! + + // ✓ CORRECT — Create a new generator each time + console.log([...nums()]) // [1, 2] + console.log([...nums()]) // [1, 2] + ``` + </Accordion> + + <Accordion title="Mistake 5: Infinite loop without break condition"> + ```javascript + // ❌ DANGER — This will hang your program + function* forever() { + let i = 0 + while (true) { + yield i++ + } + } + + const all = [...forever()] // Infinite loop trying to collect all values! + + // ✓ SAFE — Use take() or break early + function* take(n, gen) { + let count = 0 + for (const val of gen) { + if (count++ >= n) return + yield val + } + } + + const firstHundred = [...take(100, forever())] // Safe! + ``` + </Accordion> + + <Accordion title="Mistake 6: Using generators when arrays would be simpler"> + ```javascript + // ❌ OVERKILL — If you're just returning a fixed list, use an array + function* getDaysOfWeek() { + yield 'Monday' + yield 'Tuesday' + yield 'Wednesday' + yield 'Thursday' + yield 'Friday' + yield 'Saturday' + yield 'Sunday' + } + + // ✓ SIMPLER — Just use an array + const daysOfWeek = [ + 'Monday', 'Tuesday', 'Wednesday', 'Thursday', + 'Friday', 'Saturday', 'Sunday' + ] + ``` + + **Use generators when:** + - Values are computed on-demand (lazy) + - Sequence is infinite or very large + - You need to pause/resume execution + - Values come from async operations + + **Use arrays when:** + - You have a fixed, known set of values + - Values are already computed + - You need random access (`array[5]`) + </Accordion> +</AccordionGroup> + +--- + +## Key Takeaways + +<Info> +**The short version:** + +1. **Iterators** are objects with a `.next()` method that returns `{ value, done }` + +2. **Generators** are functions that pause at `yield` and resume at `.next()` + +3. **Don't forget the asterisk** — it's `function*`, not `function` + +4. **`yield` pauses, `return` ends** — and `return` values don't show up in `for...of` + +5. **`yield*` passes through** all values from another iterable + +6. **Generators are lazy** — nothing runs until you ask for it + +7. **Infinite sequences work** because generators compute on-demand + +8. **`Symbol.iterator`** is how you make objects work with `for...of` + +9. **Async generators** (`async function*`) let you `await` inside and iterate with `for await...of` + +10. **Generators are single-use** — once done, you need a fresh one +</Info> + +--- + +## Test Your Knowledge + +<AccordionGroup> + <Accordion title="Question 1: What's the difference between yield and return in a generator?"> + **Answer:** + + - `yield` **pauses** the generator and returns `{ value, done: false }`. The generator can resume from where it paused. + - `return` **ends** the generator and returns `{ value, done: true }`. The generator cannot resume. + + Important: Values from `return` are NOT included when using `for...of`, spread syntax, or `Array.from()`. + + ```javascript + function* example() { + yield 'A' // Included in iteration + yield 'B' // Included in iteration + return 'C' // NOT included in for...of! + } + + console.log([...example()]) // ['A', 'B'] + ``` + </Accordion> + + <Accordion title="Question 2: How do you make a custom object iterable?"> + **Answer:** + + Add a `[Symbol.iterator]` method that returns an iterator (an object with a `.next()` method): + + ```javascript + const myObject = { + data: [1, 2, 3], + + // Method 1: Return an iterator object + [Symbol.iterator]() { + let index = 0 + const data = this.data + return { + next() { + if (index < data.length) { + return { value: data[index++], done: false } + } + return { done: true } + } + } + } + } + + // Method 2: Use a generator (simpler!) + const myObject2 = { + data: [1, 2, 3], + + *[Symbol.iterator]() { + yield* this.data + } + } + ``` + </Accordion> + + <Accordion title="Question 3: What will this code output?"> + ```javascript + function* gen() { + console.log('A') + yield 1 + console.log('B') + yield 2 + console.log('C') + } + + const g = gen() + console.log('Start') + console.log(g.next().value) + console.log('Middle') + console.log(g.next().value) + ``` + + **Answer:** + + ``` + Start + A + 1 + Middle + B + 2 + ``` + + **Explanation:** + 1. `gen()` creates the generator but doesn't run any code + 2. `'Start'` logs + 3. First `g.next()` runs until first `yield` — logs `'A'`, returns `{ value: 1, done: false }` + 4. We log the value `1` + 5. `'Middle'` logs + 6. Second `g.next()` resumes and runs until second `yield` — logs `'B'`, returns `{ value: 2, done: false }` + 7. We log the value `2` + 8. `'C'` never logs because we didn't call `g.next()` a third time + </Accordion> + + <Accordion title="Question 4: How can you pass values INTO a generator?"> + **Answer:** + + Pass values as arguments to `.next(value)`. The value becomes the result of the `yield` expression: + + ```javascript + function* adder() { + const a = yield 'Enter first number' + const b = yield 'Enter second number' + yield `Sum: ${a + b}` + } + + const gen = adder() + console.log(gen.next().value) // "Enter first number" + console.log(gen.next(10).value) // "Enter second number" (a = 10) + console.log(gen.next(5).value) // "Sum: 15" (b = 5) + ``` + + Note: The first `.next()` starts the generator. Any value passed to it is ignored because there's no `yield` waiting to receive it yet. + </Accordion> + + <Accordion title="Question 5: When would you use an async generator?"> + **Answer:** + + Use async generators when you need to yield values from asynchronous operations: + + - **Paginated APIs** — Fetch and yield page by page + - **Streaming data** — Process chunks as they arrive + - **Database cursors** — Iterate through large result sets + - **File processing** — Read and yield lines from large files + + ```javascript + async function* fetchPages(url) { + let page = 1 + while (true) { + const response = await fetch(`${url}?page=${page}`) + const data = await response.json() + + if (data.items.length === 0) return + + yield data.items + page++ + } + } + + // Consume with for await...of + for await (const items of fetchPages('/api/products')) { + processItems(items) + } + ``` + </Accordion> + + <Accordion title="Question 6: Why can't you use [...infiniteGenerator()]?"> + **Answer:** + + Spread syntax (`...`) tries to collect ALL values into an array. With an infinite generator, this means infinite iteration. Your program will hang trying to collect infinite values. + + ```javascript + function* forever() { + let i = 0 + while (true) yield i++ + } + + // ❌ DANGER — Hangs forever! + const all = [...forever()] + + // ✓ SAFE — Limit how many you take + function* take(n, gen) { + let i = 0 + for (const val of gen) { + if (i++ >= n) return + yield val + } + } + + const first100 = [...take(100, forever())] + ``` + + Always use a limiting function like `take()`, or manually call `.next()` a specific number of times. + </Accordion> +</AccordionGroup> + +--- + +## Related Concepts + +<CardGroup cols={2}> + <Card title="Higher-Order Functions" icon="layer-group" href="/concepts/higher-order-functions"> + Generators pair nicely with map, filter, and reduce patterns + </Card> + <Card title="Promises" icon="handshake" href="/concepts/promises"> + Async generators are built on Promises + </Card> + <Card title="async/await" icon="clock" href="/concepts/async-await"> + The other half of async generators — you'll use both together + </Card> + <Card title="Event Loop" icon="arrows-spin" href="/concepts/event-loop"> + How async generators fit into JavaScript's execution model + </Card> +</CardGroup> + +--- + +## Reference + +<CardGroup cols={2}> + <Card title="Generator — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator"> + The Generator object and its methods — `.next()`, `.return()`, `.throw()` + </Card> + <Card title="Iteration Protocols — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols"> + The spec for iterators and iterables. Good for understanding what's really going on. + </Card> + <Card title="function* — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*"> + Generator function syntax and behavior + </Card> + <Card title="yield — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/yield"> + Everything about the yield operator + </Card> +</CardGroup> + +## Articles + +<CardGroup cols={2}> + <Card title="The Basics of ES6 Generators" icon="newspaper" href="https://davidwalsh.name/es6-generators"> + Kyle Simpson (You Don't Know JS) breaks down how generators work under the hood. + </Card> + <Card title="Generators — JavaScript.info" icon="newspaper" href="https://javascript.info/generators"> + Interactive tutorial with runnable examples. Great for hands-on learning. + </Card> + <Card title="Async Iterators and Generators — JavaScript.info" icon="newspaper" href="https://javascript.info/async-iterators-generators"> + Picks up where the sync guide leaves off — async generators and `for await...of`. + </Card> + <Card title="Iterators and Generators — MDN" icon="newspaper" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators"> + The official MDN walkthrough. Solid reference for both concepts. + </Card> +</CardGroup> + +## Videos + +<CardGroup cols={2}> + <Card title="Generators in JavaScript — Fun Fun Function" icon="video" href="https://www.youtube.com/watch?v=ategZqxHkz4"> + Mattias Petter Johansson makes generators fun. Seriously. + </Card> + <Card title="JavaScript Iterators and Generators — Fireship" icon="video" href="https://www.youtube.com/watch?v=IJ6EgdiI_wU"> + The fast version. 100 seconds and you'll get the gist. + </Card> + <Card title="JavaScript ES6 Generators — Traversy Media" icon="video" href="https://www.youtube.com/watch?v=dcP039DYzmE"> + Brad Traversy's walkthrough. Great if you like to code along. + </Card> +</CardGroup> diff --git a/docs/concepts/higher-order-functions.mdx b/docs/concepts/higher-order-functions.mdx new file mode 100644 index 00000000..1c15385a --- /dev/null +++ b/docs/concepts/higher-order-functions.mdx @@ -0,0 +1,951 @@ +--- +title: "Higher-Order Functions: Functions That Use Functions in JavaScript" +sidebarTitle: "Higher-Order Functions: Functions That Use Functions" +description: "Learn higher-order functions in JavaScript. Understand functions that accept or return other functions, create reusable abstractions, and write cleaner code." +--- + +What if you could tell a function *how* to do something, not just *what* data to work with? What if you could pass behavior itself as an argument, just like you pass numbers or strings? + +```javascript +// Without higher-order functions: repetitive code +for (let i = 0; i < 3; i++) { + console.log(i) +} + +// With higher-order functions: reusable abstraction +function repeat(times, action) { + for (let i = 0; i < times; i++) { + action(i) + } +} + +repeat(3, console.log) // 0, 1, 2 +repeat(3, i => console.log(i * 2)) // 0, 2, 4 +``` + +This is the power of **higher-order functions**. They let you write functions that are flexible, reusable, and abstract. Instead of writing the same loop over and over with slightly different logic, you write one function and pass in the logic that changes. + +<Info> +**What you'll learn in this guide:** +- What makes a function "higher-order" +- The connection between first-class functions and HOFs +- How to create functions that accept other functions +- How to create functions that return other functions (function factories) +- How closures enable higher-order functions +- Common mistakes and how to avoid them +- When and why to use higher-order functions +</Info> + +<Warning> +**Prerequisites:** This guide assumes you understand [scope and closures](/concepts/scope-and-closures). Closures are created when higher-order functions return other functions. You should also be familiar with [callbacks](/concepts/callbacks), since callbacks are the functions being passed to higher-order functions. +</Warning> + +--- + +## What is a Higher-Order Function? + +A **higher-order function** is a function that does at least one of these two things: + +1. **Accepts one or more functions as arguments** +2. **Returns a function as its result** + +That's it. If a function takes a function or returns a function, it's higher-order. + +```javascript +// 1. Accepts a function as an argument +function doTwice(action) { + action() + action() +} + +doTwice(() => console.log('Hello!')) +// Hello! +// Hello! + +// 2. Returns a function as its result +function createGreeter(greeting) { + return function(name) { + return `${greeting}, ${name}!` + } +} + +const sayHello = createGreeter('Hello') +console.log(sayHello('Alice')) // Hello, Alice! +console.log(sayHello('Bob')) // Hello, Bob! +``` + +<Tip> +**The name "higher-order"** comes from mathematics, where functions that operate on other functions are considered to be at a "higher level" of abstraction. In JavaScript, we just call them higher-order functions, or HOFs for short. +</Tip> + +### Why Does This Matter? + +Higher-order functions let you: + +- **Avoid repetition**: Write the structure once, vary the behavior +- **Create abstractions**: Hide complexity behind simple interfaces +- **Build reusable utilities**: Functions that work with any logic you pass them +- **Compose functionality**: Combine simple functions into complex ones + +Without higher-order functions, you'd repeat the same patterns over and over. With them, you write flexible code that adapts to different needs. + +--- + +## The Pea Soup Analogy + +To understand why higher-order functions matter, let's look at an analogy from *Eloquent JavaScript*. + +Compare these two recipes for pea soup: + +**Recipe 1 (Low-level instructions):** + +> Put 1 cup of dried peas per person into a container. Add water until the peas are well covered. Leave the peas in water for at least 12 hours. Take the peas out of the water and put them in a cooking pan. Add 4 cups of water per person. Cover the pan and keep the peas simmering for two hours. Take half an onion per person. Cut it into pieces with a knife. Add it to the peas... + +**Recipe 2 (Higher-level instructions):** + +> Per person: 1 cup dried split peas, 4 cups of water, half a chopped onion, a stalk of celery, and a carrot. +> +> Soak peas for 12 hours. Simmer for 2 hours. Chop and add vegetables. Cook for 10 more minutes. + +The second recipe is shorter and easier to understand. But it requires you to know what "soak", "simmer", and "chop" mean. These are **abstractions**. They hide the step-by-step details behind meaningful names. + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ LEVELS OF ABSTRACTION │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ HIGH LEVEL (What you want) │ +│ ┌───────────────────────────────────────────────────────────────┐ │ +│ │ "Calculate the area for each radius" │ │ +│ │ │ │ +│ │ radii.map(calculateArea) │ │ +│ └───────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ MEDIUM LEVEL (How to iterate) │ +│ ┌───────────────────────────────────────────────────────────────┐ │ +│ │ function map(array, transform) { │ │ +│ │ const result = [] │ │ +│ │ for (const item of array) { │ │ +│ │ result.push(transform(item)) │ │ +│ │ } │ │ +│ │ return result │ │ +│ │ } │ │ +│ └───────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ LOW LEVEL (Step by step) │ +│ ┌───────────────────────────────────────────────────────────────┐ │ +│ │ const result = [] │ │ +│ │ for (let i = 0; i < radii.length; i++) { │ │ +│ │ const radius = radii[i] │ │ +│ │ const area = Math.PI * radius * radius │ │ +│ │ result.push(area) │ │ +│ │ } │ │ +│ └───────────────────────────────────────────────────────────────┘ │ +│ │ +│ Higher-order functions let you work at the level that makes sense │ +│ for your problem, hiding the mechanical details below. │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +Higher-order functions are how we create these abstractions in JavaScript. We package up common patterns (like "do something to each item") into reusable functions, then pass in the specific behavior we need. + +--- + +## First-Class Functions: The Foundation + +Higher-order functions are possible because JavaScript has **[first-class functions](https://developer.mozilla.org/en-US/docs/Glossary/First-class_Function)**. This means functions are treated like any other value. You can: + +### 1. Assign Functions to Variables + +```javascript +// Functions are values, just like numbers or strings +const greet = function(name) { + return `Hello, ${name}!` +} + +// Arrow functions work the same way +const add = (a, b) => a + b + +console.log(greet('Alice')) // Hello, Alice! +console.log(add(2, 3)) // 5 +``` + +### 2. Pass Functions as Arguments + +```javascript +function callTwice(fn) { + fn() + fn() +} + +callTwice(function() { + console.log('This runs twice!') +}) +// This runs twice! +// This runs twice! +``` + +### 3. Return Functions from Functions + +```javascript +function createMultiplier(multiplier) { + // This returned function "remembers" the multiplier + return function(number) { + return number * multiplier + } +} + +const double = createMultiplier(2) +const triple = createMultiplier(3) + +console.log(double(5)) // 10 +console.log(triple(5)) // 15 +``` + +<Note> +**Not all languages have first-class functions.** In languages like C, you can't easily pass functions around as values. Java added lambda expressions in version 8, but they work differently than JavaScript functions. JavaScript's first-class functions make functional programming patterns natural and powerful. +</Note> + +--- + +## Higher-Order Functions That Accept Functions + +The most common type of HOF accepts a function as an argument. You pass in *what* should happen, and the HOF handles *when* and *how* it happens. + +### Example: A Reusable `repeat` Function + +Instead of writing loops everywhere, create a function that handles the looping: + +```javascript +function repeat(times, action) { + for (let i = 0; i < times; i++) { + action(i) + } +} + +// Now you can reuse this for any repeated action +repeat(3, i => console.log(`Iteration ${i}`)) +// Iteration 0 +// Iteration 1 +// Iteration 2 + +repeat(5, i => console.log('*'.repeat(i + 1))) +// * +// ** +// *** +// **** +// ***** +``` + +The `repeat` function doesn't know or care what action you want to perform. It just knows how to repeat something. You provide the "something." + +### Example: A Flexible `calculate` Function + +Suppose you need to calculate different properties of circles: + +```javascript +// Without HOF: repetitive code +function calculateAreas(radii) { + const result = [] + for (let i = 0; i < radii.length; i++) { + result.push(Math.PI * radii[i] * radii[i]) + } + return result +} + +function calculateCircumferences(radii) { + const result = [] + for (let i = 0; i < radii.length; i++) { + result.push(2 * Math.PI * radii[i]) + } + return result +} + +function calculateDiameters(radii) { + const result = [] + for (let i = 0; i < radii.length; i++) { + result.push(2 * radii[i]) + } + return result +} +``` + +That's a lot of repetition! The only thing that changes is the formula. Let's use a higher-order function: + +```javascript +// With HOF: write the loop once, pass in the logic +function calculate(radii, formula) { + const result = [] + for (const radius of radii) { + result.push(formula(radius)) + } + return result +} + +// Define the specific logic separately +const area = r => Math.PI * r * r +const circumference = r => 2 * Math.PI * r +const diameter = r => 2 * r + +const radii = [1, 2, 3] + +console.log(calculate(radii, area)) +// [3.14159..., 12.56637..., 28.27433...] + +console.log(calculate(radii, circumference)) +// [6.28318..., 12.56637..., 18.84955...] + +console.log(calculate(radii, diameter)) +// [2, 4, 6] +``` + +Now adding a new calculation is easy. Just write a new formula function: + +```javascript +// Works for any formula that takes a radius! +const squaredRadius = r => r * r +console.log(calculate(radii, squaredRadius)) // [1, 4, 9] +``` + +### Example: An `unless` Function + +You can create new control flow abstractions: + +```javascript +function unless(condition, action) { + if (!condition) { + action() + } +} + +// Use it to express "do this unless that" +repeat(5, n => { + unless(n % 2 === 1, () => { + console.log(n, 'is even') + }) +}) +// 0 is even +// 2 is even +// 4 is even +``` + +This reads almost like English: "Unless n is odd, log that it's even." + +--- + +## Higher-Order Functions That Return Functions + +The second type of HOF returns a function. This is powerful because the returned function can "remember" values from when it was created. + +### Example: The `greaterThan` Factory + +```javascript +function greaterThan(n) { + return function(m) { + return m > n + } +} + +const greaterThan10 = greaterThan(10) +const greaterThan100 = greaterThan(100) + +console.log(greaterThan10(11)) // true +console.log(greaterThan10(5)) // false +console.log(greaterThan100(50)) // false +console.log(greaterThan100(150)) // true +``` + +`greaterThan` is a **function factory**. You give it a number, and it manufactures a new function that tests if other numbers are greater than that number. + +### Example: The `multiplier` Factory + +```javascript +function multiplier(factor) { + return number => number * factor +} + +const double = multiplier(2) +const triple = multiplier(3) +const tenX = multiplier(10) + +console.log(double(5)) // 10 +console.log(triple(5)) // 15 +console.log(tenX(5)) // 50 + +// You can use the factory directly too +console.log(multiplier(7)(3)) // 21 +``` + +### Example: A `noisy` Wrapper + +Higher-order functions can wrap other functions to add behavior: + +```javascript +function noisy(fn) { + return function(...args) { + console.log('Calling with arguments:', args) + const result = fn(...args) + console.log('Returned:', result) + return result + } +} + +const noisyMax = noisy(Math.max) + +noisyMax(3, 1, 4, 1, 5) +// Calling with arguments: [3, 1, 4, 1, 5] +// Returned: 5 + +const noisyFloor = noisy(Math.floor) + +noisyFloor(4.7) +// Calling with arguments: [4.7] +// Returned: 4 +``` + +The original functions (`Math.max`, `Math.floor`) are unchanged. We've created new functions that log their inputs and outputs, wrapping the original behavior. + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ THE WRAPPER PATTERN │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Original Function Wrapped Function │ +│ ┌─────────────────┐ ┌─────────────────────────────────┐ │ +│ │ │ │ 1. Log the arguments │ │ +│ │ Math.max │ noisy() │ 2. Call Math.max │ │ +│ │ │ ────────► │ 3. Log the result │ │ +│ │ (3,1,4,1,5) → 5 │ │ 4. Return the result │ │ +│ │ │ │ │ │ +│ └─────────────────┘ └─────────────────────────────────┘ │ +│ │ +│ The wrapper adds behavior before and after, without changing │ +│ the original function. This is the "decorator" pattern. │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Function Factories in Practice + +Function factories are functions that create and return other functions. They're useful when you need many similar functions that differ only in some configuration. + +### Example: Creating Validators + +```javascript +function createValidator(min, max) { + return function(value) { + return value >= min && value <= max + } +} + +const isValidAge = createValidator(0, 120) +const isValidPercentage = createValidator(0, 100) +const isValidRating = createValidator(1, 5) + +console.log(isValidAge(25)) // true +console.log(isValidAge(150)) // false +console.log(isValidPercentage(50)) // true +console.log(isValidPercentage(101)) // false +console.log(isValidRating(3)) // true +``` + +### Example: Creating Formatters + +```javascript +function createFormatter(prefix, suffix) { + return function(value) { + return `${prefix}${value}${suffix}` + } +} + +const formatDollars = createFormatter('$', '') +const formatPercent = createFormatter('', '%') +const formatParens = createFormatter('(', ')') + +console.log(formatDollars(99.99)) // $99.99 +console.log(formatPercent(75)) // 75% +console.log(formatParens('aside')) // (aside) +``` + +### Example: Pre-filling Arguments (Partial Application) + +```javascript +function partial(fn, ...presetArgs) { + return function(...laterArgs) { + return fn(...presetArgs, ...laterArgs) + } +} + +function greet(greeting, punctuation, name) { + return `${greeting}, ${name}${punctuation}` +} + +const sayHello = partial(greet, 'Hello', '!') +const askHowAreYou = partial(greet, 'How are you', '?') + +console.log(sayHello('Alice')) // Hello, Alice! +console.log(sayHello('Bob')) // Hello, Bob! +console.log(askHowAreYou('Charlie')) // How are you, Charlie? +``` + +--- + +## The Closure Connection + +Higher-order functions that return functions rely on **[closures](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures)**. When a function is created inside another function, it "closes over" the variables in its surrounding scope, remembering them even after the outer function has finished. + +```javascript +function createCounter(start = 0) { + let count = start // This variable is "enclosed" + + return function() { + count++ // The inner function can access and modify it + return count + } +} + +const counter1 = createCounter() +const counter2 = createCounter(100) + +console.log(counter1()) // 1 +console.log(counter1()) // 2 +console.log(counter1()) // 3 + +console.log(counter2()) // 101 +console.log(counter2()) // 102 + +// Each counter has its own private count variable +console.log(counter1()) // 4 (not affected by counter2) +``` + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ HOW CLOSURES WORK WITH HOFs │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ createCounter(0) createCounter(100) │ +│ ┌─────────────────────┐ ┌─────────────────────┐ │ +│ │ count = 0 │ │ count = 100 │ │ +│ │ │ │ │ │ +│ │ ┌───────────────┐ │ │ ┌───────────────┐ │ │ +│ │ │ function() { │ │ │ │ function() { │ │ │ +│ │ │ count++ │◄─┼───────┐ │ │ count++ │◄─┼───────┐ │ +│ │ │ return count│ │ │ │ │ return count│ │ │ │ +│ │ │ } │ │ │ │ │ } │ │ │ │ +│ │ └───────────────┘ │ │ │ └───────────────┘ │ │ │ +│ └─────────────────────┘ │ └─────────────────────┘ │ │ +│ │ │ │ │ │ +│ ▼ │ ▼ │ │ +│ counter1 ───────────────┘ counter2 ───────────────┘ │ +│ │ +│ Each returned function has its own "backpack" containing the │ +│ variables from when it was created. This is a closure. │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### Private Variables Through Closures + +This pattern creates truly private variables. Nothing outside can access `count` directly: + +```javascript +function createBankAccount(initialBalance) { + let balance = initialBalance // Private variable + + return { + deposit(amount) { + if (amount > 0) { + balance += amount + return balance + } + }, + withdraw(amount) { + if (amount > 0 && amount <= balance) { + balance -= amount + return balance + } + return 'Insufficient funds' + }, + getBalance() { + return balance + } + } +} + +const account = createBankAccount(100) +console.log(account.getBalance()) // 100 +console.log(account.deposit(50)) // 150 +console.log(account.withdraw(30)) // 120 + +// Can't access balance directly +console.log(account.balance) // undefined +``` + +--- + +## Built-in Higher-Order Functions + +JavaScript provides many built-in higher-order functions, especially for working with arrays. These are covered in depth in the [Map, Reduce, and Filter](/concepts/map-reduce-filter) guide, but here's a quick overview: + +| Method | What it does | Returns | +|--------|--------------|---------| +| [`forEach(fn)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach) | Calls `fn` on each element | `undefined` | +| [`map(fn)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) | Transforms each element with `fn` | New array | +| [`filter(fn)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter) | Keeps elements where `fn` returns `true` | New array | +| [`reduce(fn, init)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce) | Accumulates elements into single value | Single value | +| [`find(fn)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find) | Returns first element where `fn` returns `true` | Element or `undefined` | +| [`some(fn)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some) | Tests if any element passes `fn` | `boolean` | +| [`every(fn)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/every) | Tests if all elements pass `fn` | `boolean` | +| [`sort(fn)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) | Sorts elements using comparator `fn` | Sorted array (mutates!) | + +```javascript +const numbers = [1, 2, 3, 4, 5] + +// All of these accept a function as an argument +numbers.forEach(n => console.log(n)) // Logs each number +numbers.map(n => n * 2) // [2, 4, 6, 8, 10] +numbers.filter(n => n > 2) // [3, 4, 5] +numbers.reduce((sum, n) => sum + n, 0) // 15 +numbers.find(n => n > 3) // 4 +numbers.some(n => n > 4) // true +numbers.every(n => n > 0) // true +``` + +<Note> +For a deep dive into these methods with practical examples, see [Map, Reduce, and Filter](/concepts/map-reduce-filter). +</Note> + +--- + +## Common Mistakes + +### 1. Forgetting to Return in Arrow Functions + +When using curly braces in arrow functions, you must explicitly `return`: + +```javascript +// ❌ WRONG - implicit return only works without braces +const double = numbers.map(n => { + n * 2 // This doesn't return anything! +}) +console.log(double) // [undefined, undefined, undefined, ...] + +// ✓ CORRECT - explicit return with braces +const double = numbers.map(n => { + return n * 2 +}) + +// ✓ CORRECT - implicit return without braces +const double = numbers.map(n => n * 2) +``` + +### 2. Losing `this` Context + +When passing methods as callbacks, `this` may not be what you expect: + +```javascript +const user = { + name: 'Alice', + greet() { + console.log(`Hello, I'm ${this.name}`) + } +} + +// ❌ WRONG - 'this' is lost +setTimeout(user.greet, 1000) // "Hello, I'm undefined" + +// ✓ CORRECT - bind the context +setTimeout(user.greet.bind(user), 1000) // "Hello, I'm Alice" + +// ✓ CORRECT - use an arrow function wrapper +setTimeout(() => user.greet(), 1000) // "Hello, I'm Alice" +``` + +### 3. The `parseInt` Gotcha with `map` + +`map` passes three arguments to its callback: `(element, index, array)`. Some functions don't expect this: + +```javascript +// ❌ WRONG - parseInt receives (string, index) and uses index as radix +['1', '2', '3'].map(parseInt) // [1, NaN, NaN] + +// Why? map calls: +// parseInt('1', 0) → 1 (radix 0 is treated as 10) +// parseInt('2', 1) → NaN (radix 1 is invalid) +// parseInt('3', 2) → NaN (3 is not valid in binary) + +// ✓ CORRECT - wrap parseInt to only pass the string +['1', '2', '3'].map(str => parseInt(str, 10)) // [1, 2, 3] + +// ✓ CORRECT - use Number instead +['1', '2', '3'].map(Number) // [1, 2, 3] +``` + +### 4. Using Higher-Order Functions When a Simple Loop is Clearer + +Don't force HOFs when a simple loop would be clearer: + +```javascript +// Sometimes this is clearer... +let sum = 0 +for (const n of numbers) { + sum += n +} + +// ...than this (for simple cases) +const sum = numbers.reduce((acc, n) => acc + n, 0) +``` + +Use HOFs when they make the code more readable, not just to seem clever. + +--- + +## Key Takeaways + +<Info> +**The key things to remember:** + +1. **A higher-order function** accepts functions as arguments OR returns a function. If it does either, it's higher-order. + +2. **First-class functions** make HOFs possible. In JavaScript, functions are values you can assign, pass, and return. + +3. **HOFs that accept functions** let you parameterize behavior. Write the structure once, pass in what varies. + +4. **HOFs that return functions** create function factories. They "manufacture" specialized functions from a template. + +5. **Closures are the key** to functions returning functions. The returned function remembers variables from when it was created. + +6. **Built-in array methods** like `map`, `filter`, `reduce`, `forEach`, `find`, `some`, and `every` are all higher-order functions. + +7. **The abstraction benefit** is huge. HOFs let you work at the right level of abstraction, hiding mechanical details. + +8. **Watch out for common gotchas** like losing `this`, forgetting to return, and unexpected arguments like with `parseInt`. + +9. **Don't overuse HOFs**. Sometimes a simple loop is clearer. Use HOFs when they make code more readable, not less. +</Info> + +--- + +## Test Your Knowledge + +<AccordionGroup> + <Accordion title="What makes a function 'higher-order'?"> + **Answer:** + + A function is higher-order if it does at least one of these two things: + + 1. Accepts one or more functions as arguments + 2. Returns a function as its result + + ```javascript + // Accepts a function + function doTwice(fn) { + fn() + fn() + } + + // Returns a function + function multiplier(factor) { + return n => n * factor + } + + // Does both! + function compose(f, g) { + return x => f(g(x)) + } + ``` + </Accordion> + + <Accordion title="What's the relationship between callbacks and higher-order functions?"> + **Answer:** + + They're two sides of the same coin: + + - A **callback** is a function passed to another function to be executed later + - A **higher-order function** is a function that accepts (or returns) other functions + + When you pass a callback to a higher-order function, the HOF decides when to call it. + + ```javascript + // setTimeout is a higher-order function + // The arrow function is the callback + setTimeout(() => console.log('Done!'), 1000) + + // map is a higher-order function + // n => n * 2 is the callback + [1, 2, 3].map(n => n * 2) + ``` + </Accordion> + + <Accordion title="Why does ['1','2','3'].map(parseInt) return [1, NaN, NaN]?"> + **Answer:** + + `map` passes three arguments to its callback: `(element, index, array)`. + + `parseInt` accepts two arguments: `(string, radix)`. So `map` accidentally passes the index as the radix: + + ```javascript + // What map actually calls: + parseInt('1', 0) // 1 (radix 0 → default base 10) + parseInt('2', 1) // NaN (radix 1 is invalid) + parseInt('3', 2) // NaN (3 is not valid binary) + ``` + + The fix is to wrap `parseInt`: + + ```javascript + ['1', '2', '3'].map(str => parseInt(str, 10)) // [1, 2, 3] + // or + ['1', '2', '3'].map(Number) // [1, 2, 3] + ``` + </Accordion> + + <Accordion title="How do closures enable function factories?"> + **Answer:** + + When a function returns another function, the inner function "closes over" variables from the outer function's scope. It remembers them even after the outer function has finished. + + ```javascript + function createMultiplier(factor) { + // 'factor' is captured by the returned function + return function(number) { + return number * factor + } + } + + const double = createMultiplier(2) // factor = 2 is remembered + const triple = createMultiplier(3) // factor = 3 is remembered + + console.log(double(5)) // 10 (uses factor = 2) + console.log(triple(5)) // 15 (uses factor = 3) + ``` + + Each returned function has its own closure with its own `factor` value. + </Accordion> + + <Accordion title="When should you NOT use higher-order functions?"> + **Answer:** + + Avoid HOFs when: + + 1. **A simple loop is clearer** for your specific case + 2. **Performance is critical** (loops can be faster for simple operations) + 3. **The abstraction adds more complexity** than it removes + 4. **You're chaining too many operations** making debugging hard + + ```javascript + // Sometimes this is perfectly fine: + let sum = 0 + for (const n of numbers) { + sum += n + } + + // Don't force this just to use HOFs: + const sum = numbers.reduce((acc, n) => acc + n, 0) + ``` + + The goal is readable, maintainable code. Use whatever achieves that. + </Accordion> + + <Accordion title="What's the difference between map() and forEach()?"> + **Answer:** + + | Aspect | `map()` | `forEach()` | + |--------|---------|-------------| + | Returns | New array with transformed elements | `undefined` | + | Purpose | Transform data | Perform side effects | + | Chainable | Yes | No | + | Use when | You need the result | You just want to do something | + + ```javascript + const numbers = [1, 2, 3] + + // map: transforms and returns new array + const doubled = numbers.map(n => n * 2) + console.log(doubled) // [2, 4, 6] + + // forEach: just executes, returns undefined + const result = numbers.forEach(n => console.log(n)) + console.log(result) // undefined + ``` + + Use `map` when you need the transformed array. Use `forEach` when you just want to do something with each element (like logging or updating external state). + </Accordion> +</AccordionGroup> + +--- + +## Related Concepts + +<CardGroup cols={2}> + <Card title="Callbacks" icon="phone" href="/concepts/callbacks"> + Callbacks are functions passed to higher-order functions + </Card> + <Card title="Map, Reduce, Filter" icon="filter" href="/concepts/map-reduce-filter"> + The most common built-in higher-order functions + </Card> + <Card title="Pure Functions" icon="sparkles" href="/concepts/pure-functions"> + HOFs work best when combined with pure functions + </Card> + <Card title="Currying & Composition" icon="layer-group" href="/concepts/currying-composition"> + Advanced patterns built on top of higher-order functions + </Card> + <Card title="Scope and Closures" icon="lock" href="/concepts/scope-and-closures"> + Closures are what make functions returning functions work + </Card> +</CardGroup> + +--- + +## Reference + +<CardGroup cols={2}> + <Card title="First-class Function — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Glossary/First-class_Function"> + The foundation that makes higher-order functions possible + </Card> + <Card title="Closures — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures"> + Essential for understanding functions that return functions + </Card> + <Card title="Array Methods — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array"> + Reference for built-in higher-order array methods + </Card> + <Card title="Functions — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Functions"> + Complete guide to JavaScript functions + </Card> +</CardGroup> + +## Articles + +<CardGroup cols={2}> + <Card title="Eloquent JavaScript, Chapter 5" icon="book" href="https://eloquentjavascript.net/05_higher_order.html"> + The pea soup analogy and abstraction concepts come from this excellent free book. Includes exercises to practice HOF concepts. + </Card> + <Card title="Higher Order Functions in JavaScript — freeCodeCamp" icon="newspaper" href="https://www.freecodecamp.org/news/higher-order-functions-in-javascript-explained/"> + Practical examples with shopping carts and user data. Great step-by-step explanations of map, filter, and reduce. + </Card> + <Card title="JavaScript Array Methods — javascript.info" icon="newspaper" href="https://javascript.info/array-methods"> + Comprehensive coverage of all array HOF methods with interactive examples and exercises. + </Card> + <Card title="Understanding Higher-Order Functions — Sukhjinder Arora" icon="newspaper" href="https://blog.bitsrc.io/understanding-higher-order-functions-in-javascript-75461803bad"> + Clear explanations with practical examples showing how to create custom higher-order functions. + </Card> +</CardGroup> + +## Videos + +<CardGroup cols={2}> + <Card title="Higher Order Functions — Fun Fun Function" icon="video" href="https://www.youtube.com/watch?v=BMUiFMZr7vk"> + Part of the legendary "Functional Programming in JavaScript" series. MPJ's engaging teaching style makes HOFs click. + </Card> + <Card title="Higher-Order Functions ft. Functional Programming — Akshay Saini" icon="video" href="https://www.youtube.com/watch?v=HkWxvB1RJq0"> + Deep dive into HOFs with the calculate function example. Popular in the JavaScript community for its clear explanations. + </Card> + <Card title="JavaScript Higher Order Functions & Arrays — Traversy Media" icon="video" href="https://www.youtube.com/watch?v=rRgD1yVwIvE"> + Practical, project-based approach to understanding map, filter, reduce, and other array HOFs. + </Card> +</CardGroup> diff --git a/docs/concepts/http-fetch.mdx b/docs/concepts/http-fetch.mdx new file mode 100644 index 00000000..de5e5893 --- /dev/null +++ b/docs/concepts/http-fetch.mdx @@ -0,0 +1,1103 @@ +--- +title: "Fetch API: Making HTTP Requests the Modern Way in JavaScript" +sidebarTitle: "Fetch API: Making HTTP Requests the Modern Way" +description: "Learn how to make HTTP requests with the JavaScript Fetch API. Understand GET, POST, response handling, JSON parsing, error patterns, and AbortController for cancellation." +--- + +How does JavaScript get data from a server? How do you load user profiles, submit forms, or fetch the latest posts from an API? The answer is the **[Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)**, JavaScript's modern way to make network requests. + +```javascript +// This is how you fetch data in JavaScript +const response = await fetch('https://api.example.com/users/1') +const user = await response.json() +console.log(user.name) // "Alice" +``` + +But to understand Fetch, you need to understand what's happening underneath: **[HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP)**. + +<Info> +**What you'll learn in this guide:** +- How HTTP requests and responses work +- The five main HTTP methods (GET, POST, PUT, PATCH, DELETE) +- How to use the Fetch API to make requests +- Reading and parsing JSON responses +- The critical difference between network errors and HTTP errors +- Modern patterns with async/await +- How to cancel requests with AbortController +</Info> + +<Warning> +**Prerequisite:** This guide assumes you understand [Promises](/concepts/promises) and [async/await](/concepts/async-await). Fetch is Promise-based, so you'll need those concepts. If you're not comfortable with Promises yet, read that guide first! +</Warning> + +## What is HTTP? + +**[HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP)** (Hypertext Transfer Protocol) is the foundation of data communication on the web. It defines how messages are formatted and transmitted between clients (like web browsers) and servers. Every time you load a webpage, submit a form, or fetch data with JavaScript, HTTP is the protocol making that exchange possible. + +<Note> +**HTTP is not JavaScript.** HTTP is a language-agnostic protocol. Python, Ruby, Go, Java, and every other language uses it too. We cover HTTP basics in this guide because understanding the protocol helps with using the Fetch API effectively. If you want to dive deeper into HTTP itself, check out the MDN resources below. +</Note> + +<CardGroup cols={2}> + <Card title="HTTP — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/HTTP"> + Comprehensive guide to the HTTP protocol + </Card> + <Card title="An Overview of HTTP — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Overview"> + How HTTP works under the hood + </Card> +</CardGroup> + +--- + +## The Restaurant Analogy + +HTTP follows a simple pattern called **request-response**. To understand it, imagine you're at a restaurant: + +1. **You place an order** (the request) — "I'd like the pasta, please" +2. **The waiter takes it to the kitchen** (the network) — your order travels to where the food is prepared +3. **The kitchen prepares your meal** (the server) — they process your request and make your food +4. **The waiter brings back your food** (the response) — you receive what you asked for (hopefully!) + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ THE REQUEST-RESPONSE CYCLE │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ YOU (Browser) KITCHEN (Server) │ +│ ┌──────────┐ ┌──────────────┐ │ +│ │ │ ──── "I'd like pasta" ────► │ │ │ +│ │ :) │ (REQUEST) │ [chef] │ │ +│ │ │ │ │ │ +│ │ │ ◄──── Here you go! ──────── │ │ │ +│ │ │ (RESPONSE) │ │ │ +│ └──────────┘ └──────────────┘ │ +│ │ +│ The waiter (HTTP) is the protocol that makes this exchange work! │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +Sometimes things go wrong: +- **The kitchen is closed** (server is down) — You can't even place an order +- **They're out of pasta** (404 Not Found) — The order was received, but they can't fulfill it +- **Something's wrong in the kitchen** (500 Server Error) — They tried but something broke + +This request-response cycle is the core of how the web works. The **[Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)** is JavaScript's modern way to participate in this cycle programmatically. + +--- + +## How Does HTTP Work? + +Before diving into the Fetch API, let's understand the key concepts of HTTP itself. + +### The Request-Response Model + +Every HTTP interaction follows a simple pattern: + +<Steps> + <Step title="Client Sends Request"> + Your browser (the client) sends an HTTP request to a server. The request includes what you want (the URL), how you want it (the method), and any additional info (headers, body). + </Step> + + <Step title="Server Processes Request"> + The server receives the request, does whatever work is needed (database queries, calculations, etc.), and prepares a response. + </Step> + + <Step title="Server Sends Response"> + The server sends back an HTTP response containing a status code (success/failure), headers (metadata), and usually a body (the actual data). + </Step> + + <Step title="Client Handles Response"> + Your JavaScript code receives the response and does something with it: display data, show an error, redirect the user, etc. + </Step> +</Steps> + +### HTTP Methods: What Do You Want to Do? + +HTTP methods tell the server what action you want to perform. Think of them as verbs: + +| Method | Purpose | Restaurant Analogy | +|--------|---------|-------------------| +| **GET** | Retrieve data | "Can I see the menu?" | +| **POST** | Create new data | "I'd like to place an order" | +| **PUT** | Update/replace data | "Actually, change my order to pizza" | +| **PATCH** | Partially update data | "Add extra cheese to my order" | +| **DELETE** | Remove data | "Cancel my order" | + +```javascript +// GET - Retrieve a user +fetch('/api/users/123') + +// POST - Create a new user +fetch('/api/users', { + method: 'POST', + body: JSON.stringify({ name: 'Alice' }) +}) + +// PUT - Replace a user +fetch('/api/users/123', { + method: 'PUT', + body: JSON.stringify({ name: 'Alice Updated' }) +}) + +// PATCH - Partially update a user +fetch('/api/users/123', { + method: 'PATCH', + body: JSON.stringify({ name: 'New Name' }) +}) + +// DELETE - Remove a user +fetch('/api/users/123', { + method: 'DELETE' +}) +``` + +### HTTP Status Codes: What Happened? + +Status codes are three-digit numbers that tell you how the request went: + +<AccordionGroup> + <Accordion title="2xx - Success"> + The request was received, understood, and accepted. + + - **200 OK** — Standard success response + - **201 Created** — New resource was created (common after POST) + - **204 No Content** — Success, but nothing to return (common after DELETE) + + ```javascript + // 200 OK example + const response = await fetch('/api/users/123') + console.log(response.status) // 200 + console.log(response.ok) // true + ``` + </Accordion> + + <Accordion title="3xx - Redirection"> + The resource has moved somewhere else. + + - **301 Moved Permanently** — Resource has a new permanent URL + - **302 Found** — Temporary redirect + - **304 Not Modified** — Use your cached version + + Fetch follows redirects automatically by default. + </Accordion> + + <Accordion title="4xx - Client Errors"> + Something is wrong with your request. + + - **400 Bad Request** — Malformed request syntax + - **401 Unauthorized** — Authentication required + - **403 Forbidden** — You don't have permission + - **404 Not Found** — Resource doesn't exist + - **422 Unprocessable Entity** — Validation failed + + ```javascript + // 404 Not Found example + const response = await fetch('/api/users/999999') + console.log(response.status) // 404 + console.log(response.ok) // false + ``` + </Accordion> + + <Accordion title="5xx - Server Errors"> + Something went wrong on the server. + + - **500 Internal Server Error** — Generic server error + - **502 Bad Gateway** — Server got invalid response from upstream + - **503 Service Unavailable** — Server is overloaded or down for maintenance + + ```javascript + // 500 error example + const response = await fetch('/api/broken-endpoint') + console.log(response.status) // 500 + console.log(response.ok) // false + ``` + </Accordion> +</AccordionGroup> + +<Tip> +**Quick Rule of Thumb:** +- **2xx** = "Here's what you asked for" +- **3xx** = "Go look over there" +- **4xx** = "You messed up" +- **5xx** = "We messed up" +</Tip> + +--- + +## What is the Fetch API? + +The **[Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)** is JavaScript's modern interface for making HTTP requests. It provides a cleaner, Promise-based alternative to the older **[XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest)**, letting you send requests to servers and handle responses with simple, readable code. Every modern browser supports Fetch natively. + +```javascript +// Fetch in its simplest form +const response = await fetch('https://api.example.com/data') +const data = await response.json() +console.log(data) +``` + +### Before Fetch: The XMLHttpRequest Days + +Before Fetch existed, developers used XMLHttpRequest (XHR), a verbose, callback-based API that powered "AJAX" requests. Libraries like **[jQuery](https://jquery.com/)** became popular partly because they simplified this painful process. jQuery was revolutionary for JavaScript. For many years it was the go-to library that made DOM manipulation, animations, and AJAX requests much easier. It changed how developers wrote JavaScript and shaped the modern web. + +```javascript +// The old way: XMLHttpRequest (verbose and callback-based) +const xhr = new XMLHttpRequest() +xhr.open('GET', 'https://api.example.com/data') +xhr.onload = function() { + if (xhr.status === 200) { + const data = JSON.parse(xhr.responseText) + console.log(data) + } +} +xhr.onerror = function() { + console.error('Request failed') +} +xhr.send() + +// The modern way: Fetch (clean and Promise-based) +const response = await fetch('https://api.example.com/data') +const data = await response.json() +console.log(data) +``` + +Unlike XMLHttpRequest, Fetch: +- Returns **[Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)** instead of using callbacks +- Uses **[Request](https://developer.mozilla.org/en-US/docs/Web/API/Request)** and **[Response](https://developer.mozilla.org/en-US/docs/Web/API/Response)** objects for cleaner APIs +- Integrates naturally with **async/await** syntax +- Supports streaming responses out of the box + +<Tip> +**You no longer need jQuery for AJAX.** The Fetch API is built into every modern browser, making libraries unnecessary for basic HTTP requests. +</Tip> + +--- + +## How to Use the Fetch API + +Now that you understand what Fetch is and how it compares to older approaches, let's dive into the details of using it effectively. + +### How to Make a Fetch Request + +<Steps> + <Step title="Call fetch() with a URL"> + The `fetch()` function takes a URL and returns a Promise that resolves to a Response object. By default, it makes a GET request. + </Step> + <Step title="Check if the response was successful"> + Always verify `response.ok` before processing. Fetch doesn't throw errors for HTTP status codes like 404 or 500. + </Step> + <Step title="Parse the response body"> + Use `response.json()` for JSON data or `response.text()` for plain text. These methods return another Promise. + </Step> + <Step title="Handle errors properly"> + Wrap everything in try/catch to handle both network failures and HTTP error responses. + </Step> +</Steps> + +Here's what this looks like in code. By default, `fetch()` uses the **GET** method, so you don't need to specify it. There are two ways to write this: + +<Tabs> + <Tab title="Promise .then()"> + ```javascript + // Basic fetch - returns a Promise + fetch('https://api.example.com/users') + .then(response => response.json()) + .then(data => console.log(data)) + .catch(error => console.error('Error:', error)) + ``` + + Let's break this down step by step: + + ```javascript + // Step 1: fetch() returns a Promise that resolves to a Response object + const responsePromise = fetch('https://api.example.com/users') + + // Step 2: When the response arrives, we get a Response object + responsePromise.then(response => { + console.log(response.status) // 200 + console.log(response.ok) // true + console.log(response.headers) // Headers object + + // Step 3: The body is a stream, we need to parse it + // .json() returns ANOTHER Promise + return response.json() + }) + .then(data => { + // Step 4: Now we have the actual data + console.log(data) // { users: [...] } + }) + ``` + </Tab> + <Tab title="async/await"> + ```javascript + // Using async/await - cleaner syntax + async function getUsers() { + try { + const response = await fetch('https://api.example.com/users') + const data = await response.json() + console.log(data) + } catch (error) { + console.error('Error:', error) + } + } + ``` + + Let's break this down step by step: + + ```javascript + async function getUsers() { + // Step 1: await pauses until the Response arrives + const response = await fetch('https://api.example.com/users') + + console.log(response.status) // 200 + console.log(response.ok) // true + console.log(response.headers) // Headers object + + // Step 2: await again to read and parse the body + const data = await response.json() + + // Step 3: Now we have the actual data + console.log(data) // { users: [...] } + } + ``` + </Tab> +</Tabs> + +<Tip> +**Which should you use?** `async/await` is generally preferred for its cleaner, more readable syntax. Use `.then()` chains when you need to integrate with older codebases or when you specifically want to avoid async functions. +</Tip> + +### Understanding the Response Object + +When `fetch()` resolves, you get a **[Response](https://developer.mozilla.org/en-US/docs/Web/API/Response)** object. This object contains everything about the server's reply: status codes, headers, and methods to read the body: + +```javascript +const response = await fetch('https://api.example.com/users/1') + +// Status information +response.status // 200, 404, 500, etc. +response.statusText // "OK", "Not Found", "Internal Server Error" +response.ok // true if status is 200-299 + +// Response metadata +response.headers // Headers object +response.url // Final URL (after redirects) +response.type // "basic", "cors", etc. +response.redirected // true if response came from a redirect + +// Body methods (each returns a Promise) +response.json() // Parse body as JSON +response.text() // Parse body as plain text +response.blob() // Parse body as binary Blob +response.formData() // Parse body as FormData +response.arrayBuffer() // Parse body as ArrayBuffer +response.bytes() // Parse body as Uint8Array +``` + +<Warning> +**Important:** The body can only be read once! If you call `response.json()`, you can't call `response.text()` afterward. If you need to read it multiple times, clone the response first with `response.clone()`. +</Warning> + +### Reading JSON Data + +Most modern APIs return data in **[JSON](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON)** format. The Response object has a built-in `.json()` method that parses the body and returns a JavaScript object: + +```javascript +async function getUser(id) { + const response = await fetch(`https://api.example.com/users/${id}`) + const user = await response.json() + + console.log(user.name) // "Alice" + console.log(user.email) // "alice@example.com" + + return user +} +``` + +### Sending Data with POST + +So far we've only *retrieved* data. But what about *sending* data, like creating a user account or submitting a form? + +That's where **[POST](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST)** comes in. It's the HTTP method that tells the server "I'm sending you data to create something new." To make a POST request, you need to specify the method, set a `Content-Type` header, and include your data in the body: + +```javascript +async function createUser(userData) { + const response = await fetch('https://api.example.com/users', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(userData) + }) + + const newUser = await response.json() + return newUser +} + +// Usage +const user = await createUser({ + name: 'Bob', + email: 'bob@example.com' +}) +console.log(user.id) // New user's ID from server +``` + +### Setting Headers + +**[HTTP headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers)** are metadata you send with your request: things like authentication tokens, content types, and caching instructions. You pass them as an object in the `headers` option: + +```javascript +const response = await fetch('https://api.example.com/data', { + method: 'GET', + headers: { + // Tell server what format we want + 'Accept': 'application/json', + + // Authentication token + 'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIs...', + + // Custom header + 'X-Custom-Header': 'custom-value' + } +}) +``` + +Common headers you'll use: + +| Header | Purpose | +|--------|---------| +| `Content-Type` | Format of data you're sending (e.g., `application/json`) | +| `Accept` | Format of data you want back | +| `Authorization` | Authentication credentials | +| `Cache-Control` | Caching instructions | + +### Building URLs with Query Parameters + +When fetching data, you often need to include query parameters (e.g., `/api/search?q=javascript&page=1`). Use the **[URL](https://developer.mozilla.org/en-US/docs/Web/API/URL)** and **[URLSearchParams](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams)** APIs to build URLs safely: + +```javascript +// Building a URL with query parameters +const url = new URL('https://api.example.com/search') +url.searchParams.set('q', 'javascript') +url.searchParams.set('page', '1') +url.searchParams.set('limit', '10') + +console.log(url.toString()) +// "https://api.example.com/search?q=javascript&page=1&limit=10" + +// Use with fetch +const response = await fetch(url) +``` + +You can also use `URLSearchParams` directly: + +```javascript +const params = new URLSearchParams({ + q: 'javascript', + page: '1' +}) + +// Append to a URL string +const response = await fetch(`/api/search?${params}`) +``` + +<Tip> +**Why use URL/URLSearchParams instead of string concatenation?** These APIs automatically handle URL encoding for special characters. If a user searches for "C++ tutorial", it becomes `q=C%2B%2B+tutorial`. Something you'd have to handle manually with string concatenation. +</Tip> + +--- + +## The #1 Fetch Mistake + +Here's a mistake almost every developer makes when learning fetch: + +> "I wrapped my fetch in try/catch, so I'm handling all errors... right?" + +**Wrong.** The problem? `fetch()` only throws an error when the *network* fails, not when the server returns a 404 or 500. A "Page Not Found" response is still a successful network request from fetch's perspective! + +### Two Types of "Errors" + +When working with `fetch()`, there are two completely different types of failures: + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ TWO TYPES OF FAILURES │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ 1. NETWORK ERRORS 2. HTTP ERROR RESPONSES │ +│ ──────────────────── ─────────────────────── │ +│ │ +│ • Server unreachable • Server responded with error │ +│ • DNS lookup failed • 404 Not Found │ +│ • No internet connection • 500 Internal Server Error │ +│ • Request timed out • 401 Unauthorized │ +│ • CORS blocked • 403 Forbidden │ +│ │ +│ Promise REJECTS ❌ Promise RESOLVES ✓ │ +│ Goes to .catch() response.ok is false │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +<Warning> +**The Trap:** `fetch()` only rejects its Promise for network errors. An HTTP 404 or 500 response is still a "successful" fetch. The network request completed! You must check `response.ok` to detect HTTP errors. +</Warning> + +### The Mistake: Only Catching Network Errors + +This code looks fine, but it has a subtle bug. HTTP errors like 404 or 500 slip right through the catch block: + +```javascript +// ❌ WRONG - This misses HTTP errors! +try { + const response = await fetch('/api/users/999') + const data = await response.json() + console.log(data) // Might be an error object! +} catch (error) { + // Only catches NETWORK errors + // A 404 response WON'T end up here! + console.error('Error:', error) +} +``` + +### The Fix: Always Check response.ok + +The solution is simple: check `response.ok` before assuming success. This property is `true` for status codes 200-299 and `false` for everything else: + +```javascript +// ✓ CORRECT - Check response.ok +async function fetchUser(id) { + try { + const response = await fetch(`/api/users/${id}`) + + // Check if the HTTP response was successful + if (!response.ok) { + // HTTP error (4xx, 5xx) - throw to catch block + throw new Error(`HTTP error! Status: ${response.status}`) + } + + const data = await response.json() + return data + + } catch (error) { + // Now this catches BOTH network errors AND HTTP errors + console.error('Fetch failed:', error.message) + throw error + } +} +``` + +### Building a Reusable Fetch Helper + +Here's a pattern you can use in real projects: a wrapper function that handles the `response.ok` check for you: + +```javascript +async function fetchJSON(url, options = {}) { + const response = await fetch(url, { + headers: { + 'Content-Type': 'application/json', + ...options.headers + }, + ...options + }) + + // Handle HTTP errors + if (!response.ok) { + const error = new Error(`HTTP ${response.status}: ${response.statusText}`) + error.status = response.status + error.response = response + throw error + } + + // Handle empty responses (like 204 No Content) + if (response.status === 204) { + return null + } + + return response.json() +} + +// Usage +try { + const user = await fetchJSON('/api/users/1') + console.log(user) +} catch (error) { + if (error.status === 404) { + console.log('User not found') + } else if (error.status >= 500) { + console.log('Server error, try again later') + } else { + console.log('Request failed:', error.message) + } +} +``` + +--- + +## How to Use async/await with Fetch + +The examples above use `.then()` chains, but modern JavaScript has a cleaner syntax: `async/await`. If you're not familiar with it, check out our [async/await concept](/concepts/async-await) first. It'll make your fetch code much easier to read. + +### Basic async/await Pattern + +```javascript +async function loadUserProfile(userId) { + try { + const response = await fetch(`/api/users/${userId}`) + + if (!response.ok) { + throw new Error(`Failed to load user: ${response.status}`) + } + + const user = await response.json() + return user + + } catch (error) { + console.error('Error loading profile:', error) + return null + } +} + +// Usage +const user = await loadUserProfile(123) +if (user) { + console.log(`Welcome, ${user.name}!`) +} +``` + +### Parallel Requests + +Need to fetch multiple resources? Don't await them one by one: + +```javascript +// ❌ SLOW - Sequential requests (one after another) +async function loadDashboardSlow() { + const user = await fetch('/api/user').then(r => r.json()) + const posts = await fetch('/api/posts').then(r => r.json()) + const notifications = await fetch('/api/notifications').then(r => r.json()) + // Total time: user + posts + notifications + return { user, posts, notifications } +} + +// ✓ FAST - Parallel requests (all at once) +async function loadDashboardFast() { + const [user, posts, notifications] = await Promise.all([ + fetch('/api/user').then(r => r.json()), + fetch('/api/posts').then(r => r.json()), + fetch('/api/notifications').then(r => r.json()) + ]) + // Total time: max(user, posts, notifications) + return { user, posts, notifications } +} +``` + +### Loading States Pattern + +In real applications, you need to track loading and error states: + +```javascript +async function fetchWithState(url) { + const state = { + data: null, + loading: true, + error: null + } + + try { + const response = await fetch(url) + + if (!response.ok) { + throw new Error(`HTTP ${response.status}`) + } + + state.data = await response.json() + } catch (error) { + state.error = error.message + } finally { + state.loading = false + } + + return state +} + +// Usage +const result = await fetchWithState('/api/users') + +if (result.loading) { + console.log('Loading...') +} else if (result.error) { + console.log('Error:', result.error) +} else { + console.log('Data:', result.data) +} +``` + +--- + +## How to Cancel Requests + +The **[AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController)** API lets you cancel in-flight fetch requests. This is useful for: + +- **Timeouts** — Cancel requests that take too long +- **User navigation** — Cancel pending requests when user leaves a page +- **Search inputs** — Cancel the previous search when user types new characters +- **Component cleanup** — Cancel requests when a React/Vue component unmounts + +Without AbortController, abandoned requests continue running in the background, wasting bandwidth and potentially causing bugs when their responses arrive after you no longer need them. + +### How It Works + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ ABORTCONTROLLER FLOW │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ 1. Create controller 2. Pass signal to fetch │ +│ ┌─────────────────────┐ ┌─────────────────────────────────┐ │ +│ │ const controller = │ │ fetch(url, { │ │ +│ │ new AbortController│ ───► │ signal: controller.signal │ │ +│ └─────────────────────┘ │ }) │ │ +│ └─────────────────────────────────┘ │ +│ │ +│ 3. Call abort() to cancel 4. Fetch rejects with AbortError │ +│ ┌─────────────────────┐ ┌─────────────────────────────────┐ │ +│ │ controller.abort() │ ───► │ catch (error) { │ │ +│ └─────────────────────┘ │ error.name === 'AbortError' │ │ +│ │ } │ │ +│ └─────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### Basic AbortController Usage + +```javascript +// Create a controller +const controller = new AbortController() + +// Pass its signal to fetch +fetch('/api/slow-endpoint', { + signal: controller.signal +}) + .then(response => response.json()) + .then(data => console.log(data)) + .catch(error => { + if (error.name === 'AbortError') { + console.log('Request was cancelled') + } else { + console.error('Request failed:', error) + } + }) + +// Cancel the request after 5 seconds +setTimeout(() => { + controller.abort() +}, 5000) +``` + +### Timeout Pattern + +Create a reusable timeout wrapper: + +```javascript +async function fetchWithTimeout(url, options = {}, timeout = 5000) { + const controller = new AbortController() + + // Set up timeout + const timeoutId = setTimeout(() => { + controller.abort() + }, timeout) + + try { + const response = await fetch(url, { + ...options, + signal: controller.signal + }) + + clearTimeout(timeoutId) + return response + + } catch (error) { + clearTimeout(timeoutId) + + if (error.name === 'AbortError') { + throw new Error(`Request timed out after ${timeout}ms`) + } + + throw error + } +} + +// Usage +try { + const response = await fetchWithTimeout('/api/data', {}, 3000) + const data = await response.json() +} catch (error) { + console.error(error.message) // "Request timed out after 3000ms" +} +``` + +### Search Input Pattern + +Cancel previous search when user types: + +```javascript +let currentController = null + +async function searchUsers(query) { + // Cancel any in-flight request + if (currentController) { + currentController.abort() + } + + // Create new controller for this request + currentController = new AbortController() + + try { + const response = await fetch(`/api/search?q=${query}`, { + signal: currentController.signal + }) + + if (!response.ok) throw new Error('Search failed') + + return await response.json() + + } catch (error) { + if (error.name === 'AbortError') { + // Ignore - we cancelled this on purpose + return null + } + throw error + } +} + +// As user types, only the last request matters +searchInput.addEventListener('input', async (e) => { + const results = await searchUsers(e.target.value) + if (results) { + displayResults(results) + } +}) +``` + +<Note> +This example uses browser DOM APIs (`addEventListener`, `searchInput`). In Node.js or server-side contexts, you would trigger the search function differently, but the AbortController pattern remains the same. +</Note> + +--- + +## Key Takeaways + +<Info> +**The key things to remember:** + +1. **HTTP is request-response** — Client sends a request, server sends a response + +2. **HTTP methods are verbs** — GET (read), POST (create), PUT (update), DELETE (remove) + +3. **Status codes tell you what happened** — 2xx (success), 4xx (your fault), 5xx (server's fault) + +4. **Fetch returns a Promise** — It resolves to a Response object, not directly to data + +5. **Response.json() is also a Promise** — You need to await it too + +6. **Fetch only rejects on network errors** — HTTP 404/500 still "succeeds" — check `response.ok`! + +7. **Always check response.ok** — This is the most common fetch mistake + +8. **Use async/await** — It's cleaner than Promise chains + +9. **Use Promise.all for parallel requests** — Don't await sequentially when you don't have to + +10. **AbortController cancels requests** — Useful for search inputs and cleanup +</Info> + +--- + +## Test Your Knowledge + +<AccordionGroup> + <Accordion title="Question 1: What's the difference between a network error and an HTTP error in fetch?"> + **Answer:** + + - **Network errors** occur when the request can't be completed at all — server unreachable, DNS failure, no internet, CORS blocked, etc. These cause the fetch Promise to **reject**. + + - **HTTP errors** occur when the server responds with an error status code (4xx, 5xx). The request completed successfully (the network worked), so the Promise **resolves**. You must check `response.ok` to detect these. + + ```javascript + try { + const response = await fetch('/api/data') + + // This line runs even for 404, 500, etc.! + if (!response.ok) { + throw new Error(`HTTP ${response.status}`) + } + + const data = await response.json() + } catch (error) { + // Now catches both types + } + ``` + </Accordion> + + <Accordion title="Question 2: Why does response.json() return a Promise?"> + **Answer:** The response body is a readable stream that might still be downloading when `fetch()` resolves. The `response.json()` method reads the entire stream and parses it as JSON, which is an asynchronous operation. + + This is why you need to `await` it: + + ```javascript + const response = await fetch('/api/data') // Response headers arrived + const data = await response.json() // Body fully downloaded & parsed + ``` + + The same applies to `response.text()`, `response.blob()`, etc. + </Accordion> + + <Accordion title="Question 3: How do you send JSON data in a POST request?"> + **Answer:** You need to: + 1. Set the method to 'POST' + 2. Set the Content-Type header to 'application/json' + 3. Stringify your data in the body + + ```javascript + const response = await fetch('/api/users', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: 'Alice', + email: 'alice@example.com' + }) + }) + ``` + </Accordion> + + <Accordion title="Question 4: What does response.ok mean?"> + **Answer:** `response.ok` is a boolean that's `true` if the HTTP status code is in the 200-299 range (success), and `false` otherwise. + + It's a convenient shorthand for checking if the request succeeded: + + ```javascript + // These are equivalent: + if (response.ok) { ... } + if (response.status >= 200 && response.status < 300) { ... } + ``` + + Common values: + - 200, 201, 204 → `ok` is `true` + - 400, 401, 404, 500 → `ok` is `false` + </Accordion> + + <Accordion title="Question 5: How do you cancel a fetch request?"> + **Answer:** Use an `AbortController`: + + ```javascript + // 1. Create controller + const controller = new AbortController() + + // 2. Pass its signal to fetch + fetch('/api/data', { signal: controller.signal }) + .then(r => r.json()) + .catch(error => { + if (error.name === 'AbortError') { + console.log('Cancelled!') + } + }) + + // 3. Call abort() to cancel + controller.abort() + ``` + + Common use cases: + - Timeout implementation + - Cancelling when user navigates away + - Cancelling previous search when user types new input + </Accordion> + + <Accordion title="Question 6: How do you make multiple fetch requests in parallel?"> + **Answer:** Use `Promise.all()` to run requests concurrently: + + ```javascript + // ✓ Parallel - fast + const [users, posts, comments] = await Promise.all([ + fetch('/api/users').then(r => r.json()), + fetch('/api/posts').then(r => r.json()), + fetch('/api/comments').then(r => r.json()) + ]) + + // ❌ Sequential - slow (each waits for the previous) + const users = await fetch('/api/users').then(r => r.json()) + const posts = await fetch('/api/posts').then(r => r.json()) + const comments = await fetch('/api/comments').then(r => r.json()) + ``` + + Parallel requests complete in the time of the slowest request, not the sum of all requests. + </Accordion> +</AccordionGroup> + +--- + +## Related Concepts + +<CardGroup cols={2}> + <Card title="Promises" icon="handshake" href="/concepts/promises"> + Fetch is Promise-based — you need to understand Promises first + </Card> + <Card title="async/await" icon="hourglass" href="/concepts/async-await"> + Modern syntax for working with Promises and fetch + </Card> + <Card title="Event Loop" icon="arrows-spin" href="/concepts/event-loop"> + How JavaScript handles async operations like fetch + </Card> + <Card title="DOM" icon="sitemap" href="/concepts/dom"> + Often you'll fetch data and update the DOM with it + </Card> +</CardGroup> + +--- + +## Reference + +<CardGroup cols={2}> + <Card title="Fetch API — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API"> + Official MDN documentation for the Fetch API + </Card> + <Card title="Using Fetch — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch"> + Comprehensive guide to using the Fetch API + </Card> + <Card title="Response — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/Response"> + Documentation for the Response object + </Card> + <Card title="AbortController — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/AbortController"> + Documentation for cancelling fetch requests + </Card> +</CardGroup> + +## Articles + +<CardGroup cols={2}> + <Card title="How to Use Fetch with async/await" icon="newspaper" href="https://dmitripavlutin.com/javascript-fetch-async-await/"> + Breaks down fetch into 5 simple recipes you can copy-paste. Great reference when you forget the exact syntax for POST requests or headers. + </Card> + <Card title="JavaScript Fetch API Ultimate Guide" icon="newspaper" href="https://blog.webdevsimplified.com/2022-01/js-fetch-api/"> + Kyle Cook's written version of his popular YouTube tutorials. Covers GET, POST, error handling, and AbortController with the same clear teaching style. + </Card> + <Card title="Fetch API Error Handling" icon="newspaper" href="https://www.tjvantoll.com/2015/09/13/fetch-and-errors/"> + The article that explains why fetch doesn't reject on 404/500. Short read that saves you hours of debugging the "#1 fetch mistake." + </Card> +</CardGroup> + +## Videos + +<CardGroup cols={2}> + <Card title="JavaScript Fetch API" icon="video" href="https://www.youtube.com/watch?v=cuEtnrL9-H0"> + Brad builds a simple app while explaining fetch concepts. Good if you learn better by watching someone code than reading docs. + </Card> + <Card title="Learn Fetch API in 6 Minutes" icon="video" href="https://www.youtube.com/watch?v=37vxWr0WgQk"> + The fastest way to learn fetch if you're short on time. Kyle covers the essentials without any filler. + </Card> + <Card title="Async JS Crash Course - Callbacks, Promises, Async/Await" icon="video" href="https://www.youtube.com/watch?v=PoRJizFvM7s"> + Covers callbacks, Promises, and async/await before getting to fetch. Watch this if you want the full async picture, not just fetch. + </Card> +</CardGroup> diff --git a/docs/concepts/iife-modules.mdx b/docs/concepts/iife-modules.mdx new file mode 100644 index 00000000..3bc1033c --- /dev/null +++ b/docs/concepts/iife-modules.mdx @@ -0,0 +1,1499 @@ +--- +title: "IIFE, Modules & Namespaces: Structuring Code in JavaScript" +sidebarTitle: "IIFE, Modules & Namespaces: Structuring Code" +description: "Learn how to organize JavaScript code with IIFEs, namespaces, and ES6 modules. Understand private scope, exports, dynamic imports, and common module mistakes." +--- + +How do you prevent your JavaScript variables from conflicting with code from other files or libraries? How do modern applications organize thousands of lines of code across multiple files? + +```javascript +// Modern JavaScript: Each file is its own module +// utils.js +export function formatDate(date) { + return date.toLocaleDateString() +} + +// main.js +import { formatDate } from './utils.js' +console.log(formatDate(new Date())) // "12/30/2025" +``` + +This is **[ES6 modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules)**. It's JavaScript's built-in way to organize code into separate files, each with its own private scope. But before modules existed, developers invented clever patterns like **IIFEs** and **namespaces** to solve the same problems. + +<Info> +**What you'll learn in this guide:** +- What IIFEs are and why they were invented +- How to create private variables and avoid global pollution +- What namespaces are and how to use them +- Modern ES6 modules: import, export, and organizing large projects +- The evolution from IIFEs to modules and why it matters +- Common mistakes with modules and how to avoid them +</Info> + +<Warning> +**Prerequisite:** This guide assumes you understand [scope and closures](/concepts/scope-and-closures). IIFEs and the module pattern rely on closures to create private variables. If closures feel unfamiliar, read that guide first! +</Warning> + +--- + +## What is an IIFE? + +An **[IIFE](https://developer.mozilla.org/en-US/docs/Glossary/IIFE)** (Immediately Invoked Function Expression) is a JavaScript function that runs as soon as it's defined. It creates a private scope to protect variables from polluting the global namespace. This pattern was essential before ES6 modules existed. + +```javascript +// An IIFE — runs immediately, no calling needed +(function() { + const private = "I'm hidden from the outside world"; + console.log(private); +})(); // Runs right away! + +// The variable "private" doesn't exist out here +// console.log(private); // ReferenceError: private is not defined +``` + +The parentheses around the function turn it from a declaration into an expression, and the `()` at the end immediately invokes it. This was the go-to pattern for creating private scope before JavaScript had built-in modules. + +<Note> +**Historical context:** IIFEs were everywhere in JavaScript codebases from 2010-2015. Today, most projects use ES6 modules (`import`/`export`), so you won't write many IIFEs in modern code. However, understanding them is valuable. You'll encounter IIFEs in older codebases, libraries, and they're still useful for specific cases like async initialization or quick scripts. +</Note> + +--- + +## The Messy Desk Problem: A Real-World Analogy + +Imagine you're working at a desk covered with papers, pens, sticky notes, and coffee cups. Everything is mixed together. When you need to find something specific, you have to dig through the mess. And if someone else uses your desk? Chaos. + +Now imagine organizing that desk: + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ THE MESSY DESK (No Organization) │ +│ │ +│ password = "123" userName = "Bob" calculate() │ +│ config = {} helpers = {} API_KEY = "secret" │ +│ utils = {} data = [] currentUser = null init() │ +│ │ +│ Everything is everywhere. Anyone can access anything. │ +│ Name conflicts are common. It's hard to find what you need. │ +└─────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────┐ +│ THE ORGANIZED DESK (With Modules) │ +│ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ auth.js │ │ api.js │ │ utils.js │ │ +│ │ │ │ │ │ │ │ +│ │ • login() │ │ • fetch() │ │ • format() │ │ +│ │ • logout() │ │ • post() │ │ • validate()│ │ +│ │ • user │ │ • API_KEY │ │ • helpers │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ │ +│ Each drawer has its own space. Take only what you need. │ +│ Private things stay private. Everything is easy to find. │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +This is the story of how JavaScript developers learned to organize their code: + +1. **First**, we had the messy desk — everything in the global scope +2. **Then**, we invented **IIFEs** — a clever trick to create private spaces +3. **Next**, we created **Namespaces** — grouping related things under one name +4. **Finally**, we got **Modules** — the modern, built-in solution + +Let's learn each approach and understand when to use them. + +--- + +## Part 1: IIFE — The Self-Running Function + +### Breaking Down the Name + +The acronym IIFE tells you exactly what it does: + +- **Immediately** — runs right now +- **Invoked** — called/executed +- **[Function Expression](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/function)** — a function written as an expression (not a declaration) + +```javascript +// A normal function — you define it, then call it later +function greet() { + console.log("Hello!"); +} +greet(); // You have to call it + +// An IIFE — it runs immediately, no calling needed +(function() { + console.log("Hello!"); +})(); // Runs right away! +``` + +### Expression vs Statement: Why It Matters for IIFEs + +To understand IIFEs, you need to understand the difference between **expressions** and **statements** in JavaScript. + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ EXPRESSION vs STATEMENT │ +│ │ +│ EXPRESSION = produces a value │ +│ ───────────────────────────── │ +│ 5 + 3 → 8 │ +│ "hello" → "hello" │ +│ myFunction() → whatever the function returns │ +│ x > 10 → true or false │ +│ function() {} → a function value (when in expression position)│ +│ │ +│ STATEMENT = performs an action (no value produced) │ +│ ────────────────────────────────────────────────── │ +│ if (x > 10) { } → controls flow, no value │ +│ for (let i...) { } → loops, no value │ +│ function foo() { } → declares a function, no value │ +│ let x = 5; → declares a variable, no value │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +**The key insight:** A function can be written two ways: + +```javascript +// FUNCTION DECLARATION (statement) +// Starts with the word "function" at the beginning of a line +function greet() { + return "Hello!"; +} + +// FUNCTION EXPRESSION (expression) +// The function is assigned to a variable or wrapped in parentheses +const greet = function() { + return "Hello!"; +}; +``` + +[Arrow functions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions) are always expressions: + +```javascript +const greet = () => "Hello!"; +``` + +**Why does this matter for IIFEs?** + +```javascript +// ✗ This FAILS — JavaScript sees "function" and expects a declaration +function() { + console.log("This causes a syntax error!"); +}(); // SyntaxError: Function statements require a function name + // (exact error message varies by browser) + +// ✓ This WORKS — Parentheses make it an expression +(function() { + console.log("This works!"); +})(); + +// The parentheses tell JavaScript: "This is a value, not a declaration" +``` + +<Info> +**Function Declaration vs Function Expression:** + +| Feature | Declaration | Expression | +|---------|-------------|------------| +| Syntax | `function name() {}` | `const name = function() {}` | +| [Hoisting](https://developer.mozilla.org/en-US/docs/Glossary/Hoisting) | Yes (can call before definition) | No (must define first) | +| Name | Required | Optional | +| Use in IIFE | No | Yes (must use parentheses) | +</Info> + +### The Anatomy of an IIFE + +Let's break down the syntax piece by piece: + +```javascript +(function() { + // your code here +})(); + +// Let's label each part: + +( function() { ... } ) (); +│ │ │ +│ │ └─── 3. Invoke (call) it immediately +│ │ +│ └─────── 2. Wrap in parentheses (makes it an expression) +│ +└──────────────────────────── 1. Define a function +``` + +<Tip> +**Why the parentheses?** Without them, JavaScript thinks you're writing a function declaration, not an expression. The parentheses tell JavaScript: "This is a value (an expression), not a statement." +</Tip> + +### IIFE Variations + +There are several ways to write an IIFE. They all do the same thing: + +```javascript +// Classic style +(function() { + console.log("Classic IIFE"); +})(); + +// Alternative parentheses placement +(function() { + console.log("Alternative style"); +}()); + +// Arrow function IIFE (modern) +(() => { + console.log("Arrow IIFE"); +})(); + +// With parameters +((name) => { + console.log(`Hello, ${name}!`); +})("Alice"); + +// Named IIFE (useful for debugging) +(function myIIFE() { + console.log("Named IIFE"); +})(); +``` + +### Why Were IIFEs Invented? + +Before ES6 modules, JavaScript had a big problem: **everything was global**. When scripts were loaded with regular `<script>` tags, variables declared with [`var`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/var) outside of functions became global and were shared across all scripts on the page, leading to conflicts: + +```javascript +// file1.js +var userName = "Alice"; // var creates global variables +var count = 0; + +// file2.js (loaded after file1.js) +var userName = "Bob"; // Oops! Overwrites the first userName +var count = 100; // Oops! Overwrites the first count + +// Now file1.js's code is broken because its variables were replaced +``` + +IIFEs solved this by creating a **private scope**: + +```javascript +// file1.js — wrapped in an IIFE +(function() { + var userName = "Alice"; // Private to this IIFE + var count = 0; // Private to this IIFE + + // Your code here... +})(); + +// file2.js — also wrapped in an IIFE +(function() { + var userName = "Bob"; // Different variable, no conflict! + var count = 100; // Different variable, no conflict! + + // Your code here... +})(); +``` + +### Practical Example: Creating Private Variables + +One of the most powerful uses of IIFEs is creating **private variables** that can't be accessed from outside: + +```javascript +const counter = (function() { + // Private variable — can't be accessed directly + let count = 0; // let is block-scoped, perfect for private state + + // Private function — also hidden + function log(message) { + console.log(`[Counter] ${message}`); + } + + // Return public interface + return { + increment() { + count++; + log(`Incremented to ${count}`); + }, + decrement() { + count--; + log(`Decremented to ${count}`); + }, + getCount() { + return count; + } + }; +})(); + +// Using the counter +counter.increment(); // [Counter] Incremented to 1 +counter.increment(); // [Counter] Incremented to 2 +console.log(counter.getCount()); // 2 + +// Trying to access private variables +console.log(counter.count); // undefined (it's private!) +counter.log("test"); // TypeError: counter.log is not a function +``` + +This pattern is called the **Module Pattern**. It uses [closures](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures) to keep variables private. It was the standard way to create "modules" before ES6. + +### IIFE with Parameters + +You can pass values into an IIFE: + +```javascript +// Passing jQuery to ensure $ refers to jQuery +(function($) { + // Inside here, $ is definitely jQuery + $(".button").click(function() { + console.log("Clicked!"); + }); +})(jQuery); + +// Passing window and document for performance +(function(window, document) { + // Accessing window and document is slightly faster + // because they're local variables now + const body = document.body; + const location = window.location; +})(window, document); +``` + +### When to Use IIFEs Today + +With ES6 modules, IIFEs are less common. But they're still useful for: + +<AccordionGroup> + <Accordion title="1. One-time initialization code"> + ```javascript + // Run setup code once without leaving variables behind + const config = (() => { + const env = process.env.NODE_ENV; + const apiUrl = env === 'production' + ? 'https://api.example.com' + : 'http://localhost:3000'; + + return { env, apiUrl }; + })(); + ``` + </Accordion> + + <Accordion title="2. Creating async IIFEs"> + ```javascript + // Top-level await isn't always available + // IIFE lets you use async/await anywhere + (async () => { // async functions return Promises + const response = await fetch('/api/data'); + const data = await response.json(); + console.log(data); + })(); + ``` + </Accordion> + + <Accordion title="3. Avoiding global pollution in scripts"> + ```javascript + // In a <script> tag (not a module) + (function() { + // All variables here are private + const secretKey = "abc123"; + + // Only expose what's needed + window.MyApp = { + init() { /* ... */ } + }; + })(); + ``` + </Accordion> +</AccordionGroup> + +--- + +## Part 2: Namespaces — Organizing Under One Name + +### What is a Namespace? + +A **namespace** is a container that groups related code under a single name. It's like putting all your kitchen items in a drawer labeled "Kitchen." + +```javascript +// Without namespace — variables everywhere +var userName = "Alice"; +var userAge = 25; +var userEmail = "alice@example.com"; + +function userLogin() { /* ... */ } +function userLogout() { /* ... */ } + +// With namespace — everything organized under one name +var User = { + name: "Alice", + age: 25, + email: "alice@example.com", + + login() { /* ... */ }, + logout() { /* ... */ } +}; + +// Access with the namespace prefix +console.log(User.name); +User.login(); +``` + +### Why Use Namespaces? + +``` +Before Namespaces: After Namespaces: + +Global Scope: Global Scope: +├── userName └── MyApp +├── userAge ├── User +├── userEmail │ ├── name +├── userLogin() │ ├── login() +├── userLogout() │ └── logout() +├── productName ├── Product +├── productPrice │ ├── name +├── productAdd() │ ├── price +├── cartItems │ └── add() +├── cartAdd() └── Cart +└── cartRemove() ├── items + ├── add() +11 global variables! └── remove() + + 1 global variable! +``` + +### Creating a Namespace + +The simplest namespace is just an object: + +```javascript +// Simple namespace +const MyApp = {}; + +// Add things to it +MyApp.version = "1.0.0"; +MyApp.config = { + apiUrl: "https://api.example.com", + timeout: 5000 +}; +MyApp.utils = { + formatDate(date) { + return date.toLocaleDateString(); + }, + capitalize(str) { + return str.charAt(0).toUpperCase() + str.slice(1); + } +}; + +// Use it +console.log(MyApp.version); +console.log(MyApp.utils.formatDate(new Date())); +``` + +### Nested Namespaces + +For larger applications, you can nest namespaces: + +```javascript +// Create the main namespace +const MyApp = { + // Nested namespaces + Models: {}, + Views: {}, + Controllers: {}, + Utils: {} +}; + +// Add to nested namespaces +MyApp.Models.User = { + create(name) { /* ... */ }, + find(id) { /* ... */ } +}; + +MyApp.Views.UserList = { + render(users) { /* ... */ } +}; + +MyApp.Utils.Validation = { + isEmail(str) { + return str.includes('@'); + } +}; + +// Use nested namespaces +const user = MyApp.Models.User.create("Alice"); +MyApp.Views.UserList.render([user]); +``` + +### Combining Namespaces with IIFEs + +The best of both worlds: organized AND private: + +```javascript +const MyApp = {}; + +// Use IIFE to add features with private variables +MyApp.Counter = (function() { + // Private + let count = 0; + + // Public + return { + increment() { count++; }, + decrement() { count--; }, + getCount() { return count; } + }; +})(); + +MyApp.Logger = (function() { + // Private + const logs = []; + + // Public + return { + log(message) { + logs.push({ message, time: new Date() }); + console.log(message); + }, + getLogs() { + return [...logs]; // Return a copy + } + }; +})(); + +// Usage +MyApp.Counter.increment(); +MyApp.Logger.log("Counter incremented"); +``` + +<Note> +**Namespaces vs Modules:** Namespaces are a pattern, not a language feature. They help organize code but don't provide true encapsulation. Modern ES6 modules are the preferred approach for new projects, but you'll still see namespaces in older codebases and some libraries. +</Note> + +--- + +## Part 3: ES6 Modules — The Modern Solution + +### What are Modules? + +**Modules** are JavaScript's built-in way to organize code into separate files, each with its own scope. Unlike IIFEs and namespaces (which are patterns), modules are a **language feature**. + +The [`export`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export) statement makes functions, objects, or values available to other modules. The [`import`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import) statement brings them in. + +```javascript +// math.js — A module file +export function add(a, b) { + return a + b; +} + +export function subtract(a, b) { + return a - b; +} + +export const PI = 3.14159; + +// main.js — Another module that uses math.js +import { add, subtract, PI } from './math.js'; + +console.log(add(2, 3)); // 5 +console.log(subtract(10, 4)); // 6 +console.log(PI); // 3.14159 +``` + +### Why Modules are Better + +| Feature | IIFE/Namespace | ES6 Modules | +|---------|---------------|-------------| +| File-based | No (one big file) | Yes (one module per file) | +| True privacy | Partial (IIFE only) | Yes (unexported = private) | +| Dependency management | Manual | Automatic (import/export) | +| Static analysis | No | Yes (tools can analyze) | +| Tree shaking | No | Yes (remove unused code) | +| Browser support | Always | Modern browsers + bundlers | + +### How to Use Modules + +#### In the Browser + +```html +<!-- Add type="module" to use ES6 modules --> +<script type="module" src="main.js"></script> + +<!-- Or inline --> +<script type="module"> + import { greet } from './utils.js'; + greet('World'); +</script> +``` + +#### In Node.js + +```javascript +// Option 1: Use .mjs extension +// math.mjs +export function add(a, b) { return a + b; } + +// Option 2: Add "type": "module" to package.json +// Then use .js extension normally +``` + +<Note> +**What about `require()` and `module.exports`?** You might see this older syntax in Node.js code: + +```javascript +// CommonJS (older Node.js style) +const fs = require('fs'); +module.exports = { myFunction }; +``` + +This is called **CommonJS**, Node.js's original module system. While still widely used, ES modules (`import`/`export`) are the modern standard and work in both browsers and Node.js. New projects should use ES modules. +</Note> + +--- + +## Exporting: Sharing Your Code + +There are two types of exports: **named exports** and **default exports**. + +### Named Exports + +Named exports let you export multiple things from a module. Each has a name. + +```javascript +// utils.js + +// Export as you declare +export const PI = 3.14159; + +export function square(x) { + return x * x; +} + +export class Calculator { + add(a, b) { return a + b; } +} + +// Or export at the end +const E = 2.71828; +function cube(x) { return x * x * x; } + +export { E, cube }; +``` + +### Default Export + +Each module can have ONE default export. It's the "main" thing the module provides. + +```javascript +// greeting.js + +// Default export — no name needed when importing +export default function greet(name) { + return `Hello, ${name}!`; +} + +// You can have named exports too +export const defaultName = "World"; +``` + +```javascript +// Another example — default exporting a class +// User.js + +export default class User { + constructor(name) { + this.name = name; + } + + greet() { + return `Hi, I'm ${this.name}`; + } +} +``` + +### When to Use Each + +<Tabs> + <Tab title="Named Exports"> + **Use when:** + - You're exporting multiple things + - You want clear, explicit imports + - You want to enable tree-shaking + + ```javascript + // utils.js + export function formatDate(date) { /* ... */ } + export function formatCurrency(amount) { /* ... */ } + export function formatPhone(number) { /* ... */ } + + // Import only what you need + import { formatDate } from './utils.js'; + ``` + </Tab> + + <Tab title="Default Export"> + **Use when:** + - The module has one main purpose + - You're exporting a class or component + - The import name doesn't need to match + + ```javascript + // Button.js — React component + export default function Button({ label }) { + return <button>{label}</button>; + } + + // Import with any name + import MyButton from './Button.js'; + ``` + </Tab> +</Tabs> + +--- + +## Importing: Using Other People's Code + +### Named Imports + +Import specific things by name (must match the export names): + +```javascript +// Import specific items +import { PI, square } from './utils.js'; + +// Import with a different name (alias) +import { PI as pi, square as sq } from './utils.js'; + +// Import everything as a namespace object +import * as Utils from './utils.js'; +console.log(Utils.PI); +console.log(Utils.square(4)); +``` + +### Default Import + +Import the default export with any name you choose: + +```javascript +// The name doesn't have to match the export name +import greet from './greeting.js'; + +// In a DIFFERENT file, you could use a different name: +// import sayHello from './greeting.js'; // Same function, different name +// import xyz from './greeting.js'; // Still the same function! + +// Combine default and named imports +import greet, { defaultName } from './greeting.js'; +``` + +<Tip> +**Why any name?** Default exports don't have a required name, so you choose what to call it when importing. This is useful but can make code harder to search. Named exports are often preferred for this reason. +</Tip> + +### Side-Effect Imports + +Sometimes you just want to run a module's code without importing anything: + +```javascript +// This runs the module but imports nothing +import './polyfills.js'; +import './analytics.js'; + +// Useful for: +// - Polyfills that add global features +// - Initialization code +// - CSS (with bundlers) +``` + +### Import Syntax Summary + +```javascript +// Named imports +import { a, b, c } from './module.js'; + +// Named import with alias +import { reallyLongName as short } from './module.js'; + +// Default import +import myDefault from './module.js'; + +// Default + named imports +import myDefault, { a, b } from './module.js'; + +// Import all as namespace +import * as MyModule from './module.js'; + +// Side-effect import +import './module.js'; +``` + +--- + +## Organizing a Real Project + +Let's see how modules work in a realistic project structure: + +``` +my-app/ +├── index.html +├── src/ +│ ├── main.js # Entry point +│ ├── config.js # App configuration +│ ├── utils/ +│ │ ├── index.js # Re-exports from utils +│ │ ├── format.js +│ │ └── validate.js +│ ├── services/ +│ │ ├── index.js +│ │ ├── api.js +│ │ └── auth.js +│ └── components/ +│ ├── index.js +│ ├── Button.js +│ └── Modal.js +``` + +### The Index.js Pattern (Barrel Files) + +Use `index.js` to re-export from multiple files: + +```javascript +// utils/format.js +export function formatDate(date) { /* ... */ } +export function formatCurrency(amount) { /* ... */ } + +// utils/validate.js +export function isEmail(str) { /* ... */ } +export function isPhone(str) { /* ... */ } + +// utils/index.js — re-exports everything +export { formatDate, formatCurrency } from './format.js'; +export { isEmail, isPhone } from './validate.js'; + +// Now in main.js, you can import from the folder +import { formatDate, isEmail } from './utils/index.js'; +// Or even shorter (works with bundlers and Node.js, not native browser modules): +import { formatDate, isEmail } from './utils'; +``` + +### Real Example: A Simple App + +```javascript +// config.js +export const API_URL = 'https://api.example.com'; +export const APP_NAME = 'My App'; + +// services/api.js +import { API_URL } from '../config.js'; + +export async function fetchUsers() { + const response = await fetch(`${API_URL}/users`); + return response.json(); +} + +export async function fetchPosts() { + const response = await fetch(`${API_URL}/posts`); + return response.json(); +} + +// services/auth.js +import { API_URL } from '../config.js'; + +let currentUser = null; // Private to this module + +export async function login(email, password) { + const response = await fetch(`${API_URL}/login`, { + method: 'POST', + body: JSON.stringify({ email, password }) + }); + currentUser = await response.json(); + return currentUser; +} + +export function getCurrentUser() { + return currentUser; +} + +export function logout() { + currentUser = null; +} + +// main.js — Entry point +import { APP_NAME } from './config.js'; +import { fetchUsers } from './services/api.js'; +import { login, getCurrentUser } from './services/auth.js'; + +console.log(`Welcome to ${APP_NAME}`); + +async function init() { + await login('user@example.com', 'password'); + console.log('Logged in as:', getCurrentUser().name); + + const users = await fetchUsers(); + console.log('Users:', users); +} + +init(); +``` + +--- + +## Dynamic Imports + +Sometimes you don't want to load a module until it's needed. **[Dynamic imports](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import)** load modules on demand: + +```javascript +// Static import — always loaded +import { bigFunction } from './heavy-module.js'; + +// Dynamic import — loaded only when needed +async function loadWhenNeeded() { + const module = await import('./heavy-module.js'); + module.bigFunction(); +} + +// Common use: Code splitting for routes +async function loadPage(pageName) { + switch (pageName) { + case 'home': + const home = await import('./pages/Home.js'); + return home.default; + case 'about': + const about = await import('./pages/About.js'); + return about.default; + case 'contact': + const contact = await import('./pages/Contact.js'); + return contact.default; + } +} + +// Common use: Conditional loading (inside an async function) +async function showCharts() { + if (userWantsCharts) { + const { renderChart } = await import('./chart-library.js'); + renderChart(data); + } +} +``` + +<Tip> +**Performance tip:** Dynamic imports are great for loading heavy libraries only when needed. This makes your app's initial load faster. +</Tip> + +--- + +## The Evolution: From IIFEs to Modules + +Here's how the same code would look in each era: + +<Tabs> + <Tab title="Era 1: Global (Bad)"> + ```javascript + // Everything pollutes global scope + var counter = 0; + + function increment() { + counter++; + } + + function getCount() { + return counter; + } + + // Problem: Anyone can do this + counter = 999; // Oops, state corrupted! + ``` + </Tab> + + <Tab title="Era 2: IIFE (Better)"> + ```javascript + // Uses closure to hide counter + var Counter = (function() { + var counter = 0; // Private! + + return { + increment: function() { + counter++; + }, + getCount: function() { + return counter; + } + }; + })(); + + Counter.increment(); + console.log(Counter.getCount()); // 1 + console.log(Counter.counter); // undefined (private!) + ``` + </Tab> + + <Tab title="Era 3: ES6 Modules (Best)"> + ```javascript + // counter.js + let counter = 0; // Private (not exported) + + export function increment() { + counter++; + } + + export function getCount() { + return counter; + } + + // main.js + import { increment, getCount } from './counter.js'; + + increment(); + console.log(getCount()); // 1 + // counter variable is not accessible at all + ``` + </Tab> +</Tabs> + +--- + +## Common Patterns and Best Practices + +### 1. One Thing Per Module + +Each module should do one thing well: + +```javascript +// ✗ Bad: One file does everything +// utils.js with 50 different functions + +// ✓ Good: Separate concerns +// formatters.js — formatting functions +// validators.js — validation functions +// api.js — API calls +``` + +### 2. Keep Related Things Together + +```javascript +// user/ +// ├── User.js # User class +// ├── userService.js # User API calls +// ├── userUtils.js # User-related utilities +// └── index.js # Re-exports public API +``` + +### 3. Avoid Circular Dependencies + +```javascript +// ✗ Bad: A imports B, B imports A +// a.js +import { fromB } from './b.js'; +export const fromA = "A"; + +// b.js +import { fromA } from './a.js'; // Circular! +export const fromB = "B"; + +// ✓ Good: Create a third module for shared code +// shared.js +export const sharedThing = "shared"; + +// a.js +import { sharedThing } from './shared.js'; + +// b.js +import { sharedThing } from './shared.js'; +``` + +### 4. Consider Default Exports for Components/Classes + +A common convention is to use default exports when a module has one main purpose: + +```javascript +// Components are usually one-per-file +// Button.js +export default function Button({ label, onClick }) { + return <button onClick={onClick}>{label}</button>; +} + +// Usage is clean +import Button from './Button.js'; +``` + +### 5. Use Named Exports for Utilities + +```javascript +// Multiple utilities in one file +// stringUtils.js +export function capitalize(str) { /* ... */ } +export function truncate(str, length) { /* ... */ } +export function slugify(str) { /* ... */ } + +// Import only what you need +import { capitalize } from './stringUtils.js'; +``` + +--- + +## Common Mistakes to Avoid + +### Mistake 1: Confusing Named and Default Exports + +One of the most common sources of confusion is mixing up how to import named vs default exports: + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ NAMED vs DEFAULT EXPORT CONFUSION │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ EXPORTING IMPORTING │ +│ ───────── ───────── │ +│ │ +│ Named Export: Must use { braces }: │ +│ export function greet() {} import { greet } from './mod.js' │ +│ export const PI = 3.14 import { PI } from './mod.js' │ +│ │ +│ Default Export: NO braces: │ +│ export default function() {} import greet from './mod.js' │ +│ export default class User {} import User from './mod.js' │ +│ │ +│ ⚠️ Common Error: │ +│ import greet from './mod.js' ← Looking for default, but file has │ +│ named export! Results in undefined │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +```javascript +// utils.js — has a NAMED export +export function formatDate(date) { + return date.toLocaleDateString() +} + +// ❌ WRONG — Importing without braces looks for a default export +import formatDate from './utils.js' +console.log(formatDate) // undefined! No default export exists + +// ✓ CORRECT — Use braces for named exports +import { formatDate } from './utils.js' +console.log(formatDate) // [Function: formatDate] +``` + +<Warning> +**The Trap:** If you see `undefined` when importing, check whether you're using braces correctly. Named exports require `{ }`, default exports don't. This is the #1 cause of "why is my import undefined?" bugs. +</Warning> + +### Mistake 2: Circular Dependencies + +Circular dependencies occur when two modules import from each other. This creates a "chicken and egg" problem that causes subtle, hard-to-debug issues: + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ CIRCULAR DEPENDENCY │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ user.js userUtils.js │ +│ ┌──────────┐ ┌──────────────┐ │ +│ │ │ ──── imports from ────► │ │ │ +│ │ User │ │ formatUser() │ │ +│ │ class │ ◄─── imports from ───── │ createUser() │ │ +│ │ │ │ │ │ +│ └──────────┘ └──────────────┘ │ +│ │ +│ 🔄 PROBLEM: When user.js loads, it needs userUtils.js │ +│ But userUtils.js needs User from user.js │ +│ Which isn't fully loaded yet! → undefined │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +```javascript +// ❌ PROBLEM: Circular dependency + +// user.js +import { formatUserName } from './userUtils.js' + +export class User { + constructor(name) { + this.name = name + } +} + +// userUtils.js +import { User } from './user.js' // Circular! user.js imports userUtils.js + +export function formatUserName(user) { + return user.name.toUpperCase() +} + +export function createDefaultUser() { + return new User('Guest') // 💥 User might be undefined here! +} +``` + +```javascript +// ✓ SOLUTION: Break the cycle with restructuring + +// user.js — no imports from userUtils +export class User { + constructor(name) { + this.name = name + } +} + +// userUtils.js — imports from user.js (one direction only) +import { User } from './user.js' + +export function formatUserName(user) { + return user.name.toUpperCase() +} + +export function createDefaultUser() { + return new User('Guest') // Works! User is fully loaded +} +``` + +<Tip> +**Rule of Thumb:** Draw your import arrows. They should flow in one direction like a tree, not in circles. If module A imports from B, module B should NOT import from A. If you need shared code, create a third module that both can import from. +</Tip> + +--- + +## Key Takeaways + +<Info> +**The key things to remember:** + +1. **IIFEs** create private scope by running immediately — useful for initialization and avoiding globals + +2. **Namespaces** group related code under one object — reduces global pollution but isn't true encapsulation + +3. **ES6 Modules** are the modern solution — file-based, true privacy, and built into the language + +4. **Named exports** let you export multiple things — import what you need by name + +5. **Default exports** are for the main thing a module provides — one per file + +6. **Dynamic imports** load modules on demand — great for performance optimization + +7. **Each module has its own scope** — variables are private unless exported + +8. **Use modules for new projects** — IIFEs and namespaces are for legacy code or special cases + +9. **Organize by feature or type** — group related modules in folders with index.js barrel files + +10. **Avoid circular dependencies** — they cause confusing bugs and loading issues +</Info> + +--- + +## Test Your Knowledge + +Try to answer each question before revealing the solution: + +<AccordionGroup> + <Accordion title="Question 1: What does IIFE stand for and why was it invented?"> + **Answer:** IIFE stands for **Immediately Invoked Function Expression**. + + It was invented to solve the problem of global scope pollution. Before ES6 modules, all JavaScript code shared the same global scope. Variables from different files could accidentally overwrite each other. IIFEs create a private scope where variables are protected from outside access. + </Accordion> + + <Accordion title="Question 2: What's the difference between named exports and default exports?"> + **Answer:** + + **Named exports:** + - Can have multiple per module + - Must be imported by exact name (or aliased) + - Use `export { name }` or `export function name()` + - Import with `import { name } from './module.js'` + + **Default exports:** + - Only one per module + - Can be imported with any name + - Use `export default` + - Import with `import anyName from './module.js'` + + ```javascript + // Named export + export const PI = 3.14; + import { PI } from './math.js'; + + // Default export + export default function add(a, b) { return a + b; } + import myAdd from './math.js'; // Any name works + ``` + </Accordion> + + <Accordion title="Question 3: How do you create a private variable in an IIFE?"> + **Answer:** Declare the variable inside the IIFE. It won't be accessible from outside because it's in the function's local scope. + + ```javascript + const module = (function() { + // Private variable + let privateCounter = 0; + + // Return public methods that can access it + return { + increment() { privateCounter++; }, + getCount() { return privateCounter; } + }; + })(); + + module.increment(); + console.log(module.getCount()); // 1 + console.log(module.privateCounter); // undefined (private!) + ``` + </Accordion> + + <Accordion title="Question 4: What's the difference between static and dynamic imports?"> + **Answer:** + + **Static imports:** + - Loaded at the top of the file + - Always loaded, even if not used + - Analyzed at build time + - Syntax: `import { x } from './module.js'` + + **Dynamic imports:** + - Can be loaded anywhere in the code + - Loaded only when the import() call runs + - Loaded at runtime, returns a Promise + - Syntax: `const module = await import('./module.js')` + + ```javascript + // Static import — always at the top, always loaded + import { heavyFunction } from './heavy-module.js' + + // Dynamic import — loaded only when needed + async function loadOnDemand() { + const module = await import('./heavy-module.js') + module.heavyFunction() + } + + // Or with .then() syntax + import('./heavy-module.js').then(module => { + module.heavyFunction() + }) + ``` + + Use dynamic imports for code splitting and loading modules on demand. + </Accordion> + + <Accordion title="Question 5: Why should you avoid circular dependencies?"> + **Answer:** Circular dependencies occur when module A imports from module B, and module B imports from module A. + + Problems: + - **Loading issues:** When A loads, it needs B. But B needs A, which isn't fully loaded yet. + - **Undefined values:** You might get `undefined` for imports that should have values. + - **Confusing bugs:** Hard to track down because the error isn't where the bug is. + + Solution: Create a third module for shared code, or restructure your code to break the cycle. + </Accordion> + + <Accordion title="Question 6: When would you still use an IIFE today?"> + **Answer:** Even with ES6 modules, IIFEs are useful for: + + 1. **Async initialization:** + ```javascript + (async () => { + const data = await fetchData(); + init(data); + })(); + ``` + + 2. **One-time calculations:** + ```javascript + const config = (() => { + // Complex setup that runs once + return computedConfig; + })(); + ``` + + 3. **Scripts without modules:** When you're adding a `<script>` tag without `type="module"`, IIFEs prevent polluting globals. + + 4. **Creating private scope in non-module code.** + </Accordion> +</AccordionGroup> + +--- + +## Related Concepts + +<CardGroup cols={2}> + <Card title="Scope and Closures" icon="lock" href="/concepts/scope-and-closures"> + Understanding how JavaScript manages variable access and function memory + </Card> + <Card title="Higher-Order Functions" icon="function" href="/concepts/higher-order-functions"> + Functions that work with other functions — common in modular code + </Card> + <Card title="Design Patterns" icon="compass" href="/concepts/design-patterns"> + Common patterns for organizing code, including the module pattern + </Card> + <Card title="Call Stack" icon="bars-staggered" href="/concepts/call-stack"> + How JavaScript tracks function execution and manages memory + </Card> +</CardGroup> + +--- + +## Reference + +<CardGroup cols={2}> + <Card title="IIFE — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Glossary/IIFE"> + Official MDN documentation on Immediately Invoked Function Expressions + </Card> + <Card title="JavaScript Modules — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules"> + Complete guide to ES6 modules + </Card> + <Card title="Expression Statement — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/Expression_statement"> + MDN documentation on expression statements + </Card> + <Card title="Namespace — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Glossary/Namespace"> + MDN documentation on namespaces + </Card> +</CardGroup> + +## Articles + +<CardGroup cols={2}> + <Card title="Mastering Immediately-Invoked Function Expressions" icon="newspaper" href="https://medium.com/@vvkchandra/essential-javascript-mastering-immediately-invoked-function-expressions-67791338ddc6"> + Covers the classical and Crockford IIFE variations with clear syntax breakdowns. Great for understanding why the parentheses are placed where they are. + </Card> + <Card title="JavaScript Modules: A Beginner's Guide" icon="newspaper" href="https://medium.freecodecamp.org/javascript-modules-a-beginner-s-guide-783f7d7a5fcc"> + Traces the evolution from global scripts to CommonJS to ES6 modules with code examples at each stage. Perfect if you're wondering why we have so many module formats. + </Card> + <Card title="A 10 minute primer to JavaScript modules" icon="newspaper" href="https://www.jvandemo.com/a-10-minute-primer-to-javascript-modules-module-formats-module-loaders-and-module-bundlers/"> + Explains the difference between module formats (AMD, CommonJS, ES6), loaders (RequireJS, SystemJS), and bundlers (Webpack, Rollup). Clears up the confusing terminology quickly. + </Card> + <Card title="ES6 Modules in Depth" icon="newspaper" href="https://ponyfoo.com/articles/es6-modules-in-depth"> + Nicolás Bevacqua's thorough exploration of edge cases like circular dependencies and live bindings. Read this after you understand the basics. + </Card> + <Card title="JavaScript modules — V8" icon="newspaper" href="https://v8.dev/features/modules"> + The V8 team's comprehensive guide covering native module loading, performance recommendations, and future developments. Includes practical tips on bundling vs unbundled deployment. + </Card> + <Card title="Modules — javascript.info" icon="newspaper" href="https://javascript.info/modules-intro"> + Interactive tutorial walking through module basics with live code examples. Covers both browser and Node.js usage patterns with clear, beginner-friendly explanations. + </Card> + <Card title="All you need to know about Expressions, Statements and Expression Statements" icon="newspaper" href="https://dev.to/promhize/javascript-in-depth-all-you-need-to-know-about-expressions-statements-and-expression-statements-5k2"> + Explains why `function(){}()` fails but `(function(){})()` works. The expression vs statement distinction finally makes sense after reading this. + </Card> + <Card title="Function Expressions — MDN" icon="newspaper" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/function"> + MDN's official reference on function expressions, covering syntax, hoisting behavior differences from declarations, and named function expressions. Includes interactive examples. + </Card> +</CardGroup> + +## Videos + +<CardGroup cols={2}> + <Card title="Immediately Invoked Function Expression — Beau teaches JavaScript" icon="video" href="https://www.youtube.com/watch?v=3cbiZV4H22c"> + Short and focused 4-minute explanation perfect for quick learning. Part of freeCodeCamp's beginner-friendly JavaScript series. + </Card> + <Card title="JavaScript Modules: ES6 Import and Export" icon="video" href="https://www.youtube.com/watch?v=_3oSWwapPKQ"> + Kyle from Web Dev Simplified builds a project step-by-step showing named exports, default exports, and barrel files. Great for seeing modules in action. + </Card> + <Card title="JavaScript IIFE — Steve Griffith" icon="video" href="https://www.youtube.com/watch?v=Xd7zgPFwVX8"> + Demonstrates the Module Pattern with private variables and public methods. Shows exactly how closures make IIFEs powerful. + </Card> + <Card title="ES6 Modules in the Real World" icon="video" href="https://www.youtube.com/watch?v=fIP4pjAqCtQ"> + Conference talk on practical module usage in production applications. + </Card> + <Card title="Expressions vs. Statements in JavaScript" icon="video" href="https://www.youtube.com/watch?v=WVyCrI1cHi8"> + Uses simple examples to show why expressions produce values and statements perform actions. Essential for understanding IIFE syntax. + </Card> + <Card title="JavaScript Functions — Programming with Mosh" icon="video" href="https://www.youtube.com/watch?v=N8ap4k_1QEQ"> + Comprehensive overview of JavaScript functions covering declarations, expressions, hoisting, and scope. Clear explanations with practical examples. + </Card> +</CardGroup> diff --git a/docs/concepts/inheritance-polymorphism.mdx b/docs/concepts/inheritance-polymorphism.mdx new file mode 100644 index 00000000..961f9e51 --- /dev/null +++ b/docs/concepts/inheritance-polymorphism.mdx @@ -0,0 +1,1296 @@ +--- +title: "Inheritance & Polymorphism: OOP Principles in JavaScript" +sidebarTitle: "Inheritance & Polymorphism: OOP Principles" +description: "Learn inheritance and polymorphism in JavaScript — extending classes, prototype chains, method overriding, and code reuse patterns. Master object-oriented programming principles." +--- + +How do game developers create hundreds of character types without copy-pasting the same code over and over? How can a Warrior, Mage, and Archer all "attack" differently but be treated the same way in battle? + +```javascript +// One base class, infinite possibilities +class Character { + constructor(name) { + this.name = name + this.health = 100 + } + + attack() { + return `${this.name} attacks!` + } +} + +class Warrior extends Character { + attack() { + return `${this.name} swings a mighty sword!` + } +} + +class Mage extends Character { + attack() { + return `${this.name} casts a fireball!` + } +} + +const hero = new Warrior("Aragorn") +const wizard = new Mage("Gandalf") + +console.log(hero.attack()) // "Aragorn swings a mighty sword!" +console.log(wizard.attack()) // "Gandalf casts a fireball!" +``` + +The answer lies in two powerful OOP principles: **[inheritance](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Classes_in_JavaScript#inheritance)** lets classes share code by extending other classes, and **polymorphism** lets different objects respond to the same method call in their own unique way. + +<Info> +**What you'll learn in this guide:** +- How inheritance lets child classes reuse parent class code +- Using the `extends` keyword to create class hierarchies +- The `super` keyword for calling parent constructors and methods +- Method overriding for specialized behavior +- Polymorphism: treating different object types through a common interface +- When to use composition instead of inheritance (the Gorilla-Banana problem) +- Mixins for sharing behavior across unrelated classes +</Info> + +<Warning> +**Prerequisites:** This guide assumes you understand [Factories & Classes](/concepts/factories-classes) and [Object Creation & Prototypes](/concepts/object-creation-prototypes). If you're not comfortable with creating classes in JavaScript, read those guides first! +</Warning> + +--- + +## What is Inheritance? + +**Inheritance** is a mechanism where a class (called a **child** or **subclass**) can inherit properties and methods from another class (called a **parent** or **superclass**). Instead of rewriting common functionality, the child class automatically gets everything the parent has — and can add or customize as needed. + +Think of it as the "IS-A" relationship: +- A **Warrior IS-A Character** — so it inherits all Character traits +- A **Mage IS-A Character** — same base, different specialization +- An **Archer IS-A Character** — you get the pattern + +```javascript +// The parent class — all characters share these basics +class Character { + constructor(name, health = 100) { + this.name = name + this.health = health + } + + introduce() { + return `I am ${this.name} with ${this.health} HP` + } + + attack() { + return `${this.name} attacks!` + } + + takeDamage(amount) { + this.health -= amount + return `${this.name} takes ${amount} damage! (${this.health} HP left)` + } +} + +// The child class — gets everything from Character automatically +class Warrior extends Character { + constructor(name) { + super(name, 150) // Warriors have more health! + this.rage = 0 + } + + // New method only Warriors have + battleCry() { + this.rage += 10 + return `${this.name} roars with fury! Rage: ${this.rage}` + } +} + +const conan = new Warrior("Conan") +console.log(conan.introduce()) // "I am Conan with 150 HP" (inherited!) +console.log(conan.battleCry()) // "Conan roars with fury! Rage: 10" (new!) +console.log(conan.attack()) // "Conan attacks!" (inherited!) +``` + +<Tip> +**The DRY Principle:** Inheritance helps you "Don't Repeat Yourself". Write common code once in the parent class, and all children automatically benefit — including bug fixes and improvements! +</Tip> + +--- + +## The Game Character Analogy + +Imagine you're building an RPG game. Every character — whether player or enemy — shares basic traits: a name, health points, the ability to attack and take damage. But each character *type* has unique abilities. + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ GAME CHARACTER HIERARCHY │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌───────────────┐ │ +│ │ Character │ ← Parent (base class) │ +│ │ ───────── │ │ +│ │ name │ │ +│ │ health │ │ +│ │ attack() │ │ +│ │ takeDamage() │ │ +│ └───────┬───────┘ │ +│ │ │ +│ ┌────────────────────┼────────────────────┐ │ +│ │ │ │ │ +│ ▼ ▼ ▼ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ Warrior │ │ Mage │ │ Archer │ │ +│ │ ─────── │ │ ────── │ │ ────── │ │ +│ │ rage │ │ mana │ │ arrows │ │ +│ │ battleCry()│ │ castSpell()│ │ aim() │ │ +│ │ attack() ⚔ │ │ attack() ✨│ │ attack() 🏹│ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ │ +│ Each child INHERITS from Character but OVERRIDES attack() │ +│ to provide specialized behavior — that's POLYMORPHISM! │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +Without inheritance, you'd copy-paste `name`, `health`, `takeDamage()` into every character class. With inheritance, you write it once and *extend* it: + +```javascript +class Warrior extends Character { /* ... */ } +class Mage extends Character { /* ... */ } +class Archer extends Character { /* ... */ } +``` + +Each child class automatically has everything `Character` has, plus their own unique additions. + +--- + +## Class Inheritance with `extends` + +The **[`extends`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/extends)** keyword creates a class that is a child of another class. The syntax is straightforward: + +```javascript +class ChildClass extends ParentClass { + // Child-specific code here +} +``` + +<Steps> + <Step title="Define the Parent Class"> + Create the base class with shared properties and methods: + + ```javascript + class Character { + constructor(name) { + this.name = name + this.health = 100 + } + + attack() { + return `${this.name} attacks!` + } + } + ``` + </Step> + + <Step title="Create a Child Class with extends"> + Use `extends` to inherit from the parent: + + ```javascript + class Mage extends Character { + constructor(name) { + super(name) // Call parent constructor FIRST + this.mana = 100 // Then add child-specific properties + } + + castSpell(spell) { + this.mana -= 10 + return `${this.name} casts ${spell}!` + } + } + ``` + </Step> + + <Step title="Use the Child Class"> + Instances have both parent AND child capabilities: + + ```javascript + const gandalf = new Mage("Gandalf") + + // Inherited from Character + console.log(gandalf.name) // "Gandalf" + console.log(gandalf.health) // 100 + console.log(gandalf.attack()) // "Gandalf attacks!" + + // Unique to Mage + console.log(gandalf.mana) // 100 + console.log(gandalf.castSpell("Fireball")) // "Gandalf casts Fireball!" + ``` + </Step> +</Steps> + +### What the Child Automatically Gets + +When you use `extends`, the child class inherits: + +| Inherited | Example | +|-----------|---------| +| Instance properties | `this.name`, `this.health` | +| Instance methods | `attack()`, `takeDamage()` | +| Static methods | `Character.createRandom()` (if defined) | +| Getters/Setters | `get isAlive()`, `set health(val)` | + +```javascript +class Character { + constructor(name) { + this.name = name + this.health = 100 + } + + get isAlive() { + return this.health > 0 + } + + static createRandom() { + const names = ["Hero", "Villain", "Sidekick"] + return new this(names[Math.floor(Math.random() * names.length)]) + } +} + +class Warrior extends Character { + constructor(name) { + super(name) + this.rage = 0 + } +} + +// Child inherits the static method! +const randomWarrior = Warrior.createRandom() +console.log(randomWarrior.name) // Random name +console.log(randomWarrior.isAlive) // true (inherited getter) +console.log(randomWarrior.rage) // 0 (Warrior-specific) +``` + +--- + +## The `super` Keyword + +The **[`super`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/super)** keyword is your lifeline when working with inheritance. It has two main uses: + +### 1. `super()` — Calling the Parent Constructor + +When a child class has a constructor, it **must** call `super()` before using `this`. This runs the parent's constructor to set up inherited properties. + +```javascript +class Character { + constructor(name, health) { + this.name = name + this.health = health + } +} + +class Warrior extends Character { + constructor(name) { + // MUST call super() first! + super(name, 150) // Pass arguments to parent constructor + + // Now we can use 'this' + this.rage = 0 + this.weapon = "Sword" + } +} + +const warrior = new Warrior("Conan") +console.log(warrior.name) // "Conan" (set by parent) +console.log(warrior.health) // 150 (passed to parent) +console.log(warrior.rage) // 0 (set by child) +``` + +<Warning> +**Critical Rule:** You MUST call `super()` before accessing `this` in a child constructor. If you don't, JavaScript throws a `ReferenceError`: + +```javascript +class Warrior extends Character { + constructor(name) { + this.rage = 0 // ❌ ReferenceError: Must call super constructor first! + super(name) + } +} +``` +</Warning> + +### 2. `super.method()` — Calling Parent Methods + +Use `super.methodName()` to call the parent's version of an overridden method. This is perfect when you want to *extend* behavior rather than *replace* it: + +```javascript +class Character { + constructor(name, health = 100) { + this.name = name + this.health = health + } + + attack() { + return `${this.name} attacks` + } + + describe() { + return `${this.name} (${this.health} HP)` + } +} + +class Warrior extends Character { + constructor(name) { + super(name, 150) // Pass name and custom health to parent + this.weapon = "Sword" + } + + attack() { + // Call parent's attack, then add to it + const baseAttack = super.attack() + return `${baseAttack} with a ${this.weapon}!` + } + + describe() { + // Extend parent's description + return `${super.describe()} - Warrior Class` + } +} + +const hero = new Warrior("Aragorn") +console.log(hero.attack()) // "Aragorn attacks with a Sword!" +console.log(hero.describe()) // "Aragorn (150 HP) - Warrior Class" +``` + +<Tip> +**Pattern: Extend, Don't Replace.** When overriding methods, consider calling `super.method()` first to preserve parent behavior, then add child-specific logic. This keeps your code DRY and ensures parent functionality isn't accidentally lost. +</Tip> + +--- + +## Method Overriding + +**Method overriding** occurs when a child class defines a method with the same name as one in its parent class. The child's version "shadows" the parent's version — when you call that method on a child instance, the child's implementation runs. + +```javascript +class Character { + attack() { + return `${this.name} attacks!` + } +} + +class Warrior extends Character { + attack() { + return `${this.name} swings a mighty sword for 25 damage!` + } +} + +class Mage extends Character { + attack() { + return `${this.name} hurls a fireball for 30 damage!` + } +} + +class Archer extends Character { + attack() { + return `${this.name} fires an arrow for 20 damage!` + } +} + +// Each class has the SAME method name, but DIFFERENT behavior +const warrior = new Warrior("Conan") +const mage = new Mage("Gandalf") +const archer = new Archer("Legolas") + +console.log(warrior.attack()) // "Conan swings a mighty sword for 25 damage!" +console.log(mage.attack()) // "Gandalf hurls a fireball for 30 damage!" +console.log(archer.attack()) // "Legolas fires an arrow for 20 damage!" +``` + +### Why Override Methods? + +| Reason | Example | +|--------|---------| +| **Specialization** | Each character type attacks differently | +| **Extension** | Add logging before calling `super.method()` | +| **Customization** | Change default values or behavior | +| **Performance** | Optimize for specific use case | + +### Extending vs Replacing + +You have two choices when overriding: + +<Tabs> + <Tab title="Replace Completely"> + ```javascript + class Warrior extends Character { + // Completely new implementation + attack() { + this.rage += 5 + const damage = 20 + this.rage + return `${this.name} rages and deals ${damage} damage!` + } + } + ``` + </Tab> + <Tab title="Extend Parent"> + ```javascript + class Warrior extends Character { + // Build on parent's behavior + attack() { + const base = super.attack() // "Conan attacks!" + this.rage += 5 + return `${base} Rage builds to ${this.rage}!` + } + } + ``` + </Tab> +</Tabs> + +--- + +## What is Polymorphism? + +**Polymorphism** (from Greek: "many forms") means that objects of different types can be treated through a common interface. In JavaScript, this primarily manifests as **subtype polymorphism**: child class instances can be used wherever a parent class instance is expected. + +The magic happens when you call the same method on different objects, and each responds in its own way: + +```javascript +class Character { + constructor(name) { + this.name = name + this.health = 100 + } + + attack() { + return `${this.name} attacks!` + } +} + +class Warrior extends Character { + attack() { + return `${this.name} swings a sword!` + } +} + +class Mage extends Character { + attack() { + return `${this.name} casts a spell!` + } +} + +class Archer extends Character { + attack() { + return `${this.name} shoots an arrow!` + } +} + +// THE POLYMORPHISM POWER MOVE +// This function works with ANY Character type! +function executeBattle(characters) { + console.log("⚔️ Battle begins!") + + characters.forEach(char => { + // Each character attacks in their OWN way + console.log(char.attack()) + }) +} + +// Mix of different types — polymorphism in action! +const party = [ + new Warrior("Conan"), + new Mage("Gandalf"), + new Archer("Legolas"), + new Character("Villager") // Even the base class works! +] + +executeBattle(party) +// ⚔️ Battle begins! +// "Conan swings a sword!" +// "Gandalf casts a spell!" +// "Legolas shoots an arrow!" +// "Villager attacks!" +``` + +### Why Polymorphism is Powerful + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ POLYMORPHISM: WRITE ONCE, USE MANY │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ WITHOUT Polymorphism WITH Polymorphism │ +│ ───────────────────── ───────────────── │ +│ │ +│ function battle(char) { function battle(char) { │ +│ if (char instanceof Warrior) { char.attack() // That's it! │ +│ char.swingSword() } │ +│ } else if (char instanceof // Works with Warrior, Mage, │ +│ Mage) { // Archer, and ANY future type! │ +│ char.castSpell() │ +│ } else if (char instanceof │ +│ Archer) { │ +│ char.shootArrow() │ +│ } │ +│ // Need to add code for │ +│ // every new character type! │ +│ } │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +| Benefit | Explanation | +|---------|-------------| +| **Open for Extension** | Add new character types without changing battle logic | +| **Loose Coupling** | `executeBattle` doesn't need to know about specific types | +| **Cleaner Code** | No endless `if/else` or `switch` statements | +| **Easier Testing** | Test with mock objects that share the interface | + +### The `instanceof` Operator + +Use **[`instanceof`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/instanceof)** to check if an object is an instance of a class (or its parents): + +```javascript +const warrior = new Warrior("Conan") + +console.log(warrior instanceof Warrior) // true (direct) +console.log(warrior instanceof Character) // true (parent) +console.log(warrior instanceof Object) // true (all objects) +console.log(warrior instanceof Mage) // false (different branch) +``` + +--- + +## Under the Hood: Prototypes + +Here's a secret: ES6 `class` and `extends` are **syntactic sugar** over JavaScript's prototype-based inheritance. When you write `class Warrior extends Character`, JavaScript is really setting up a prototype chain behind the scenes. + +```javascript +// What you write (ES6 class syntax) +class Character { + constructor(name) { + this.name = name + } + attack() { + return `${this.name} attacks!` + } +} + +// Note: In this example, Warrior does NOT override attack() +// This lets us see how the prototype chain lookup works +class Warrior extends Character { + constructor(name) { + super(name) + this.rage = 0 + } + + // Warrior-specific method (not on Character) + battleCry() { + return `${this.name} roars!` + } +} + +// What JavaScript actually creates (simplified) +// Warrior.prototype.__proto__ === Character.prototype +``` + +When you call `warrior.attack()`, JavaScript walks up the prototype chain: +1. Looks for `attack` on the `warrior` instance itself — not found +2. Looks on `Warrior.prototype` — not found (Warrior didn't override it) +3. Follows the chain to `Character.prototype` — **found!** Executes it + +This is why inheritance "just works" — methods defined on parent classes are automatically available to child instances through the prototype chain. + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ PROTOTYPE CHAIN │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ warrior (instance) │ +│ ┌─────────────────┐ │ +│ │ name: "Conan" │ │ +│ │ rage: 0 │ │ +│ │ [[Prototype]] ──┼──┐ │ +│ └─────────────────┘ │ │ +│ ▼ │ +│ Warrior.prototype ┌─────────────────┐ │ +│ │ battleCry() │ │ +│ │ constructor │ │ +│ │ [[Prototype]] ──┼──┐ │ +│ └─────────────────┘ │ │ +│ ▼ │ +│ Character.prototype ┌─────────────────┐ │ +│ │ attack() │ ← Found here! │ +│ │ constructor │ │ +│ │ [[Prototype]] ──┼──► Object.prototype │ +│ └─────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +<Tip> +**Rule of Thumb:** Use ES6 `class` syntax for cleaner, more readable code. Understand prototypes for debugging and advanced patterns. For a deep dive, see our [Object Creation & Prototypes](/concepts/object-creation-prototypes) guide. +</Tip> + +--- + +## Composition vs Inheritance + +Inheritance is powerful, but it's not always the right tool. There's a famous saying in programming: + +> "You wanted a banana but got a gorilla holding the banana and the entire jungle." + +This is the **Gorilla-Banana Problem** — when you inherit from a class, you inherit *everything*, even the stuff you don't need. + +### When Inheritance Goes Wrong + +```javascript +// Inheritance nightmare — deep, rigid hierarchy +class Animal { } +class Mammal extends Animal { } +class WingedMammal extends Mammal { } +class Bat extends WingedMammal { } + +// Oh no! Now we need a FlyingFish... +// Fish aren't mammals! Do we create another branch? +// What about a Penguin (bird that can't fly)? + +// The hierarchy becomes fragile and hard to change +``` + +### The "IS-A" vs "HAS-A" Test + +| Question | If Yes... | Example | +|----------|-----------|---------| +| Is a Warrior **a type of** Character? | Use inheritance | `class Warrior extends Character` | +| Does a Character **have** inventory? | Use composition | `this.inventory = new Inventory()` | + +### Composition: Building with "HAS-A" + +Instead of inheriting behavior, you **compose** objects from smaller, reusable pieces: + +<Tabs> + <Tab title="Inheritance Approach"> + ```javascript + // Rigid hierarchy — what if we need a flying warrior? + class Character { } + class FlyingCharacter extends Character { + fly() { return `${this.name} flies!` } + } + class MagicCharacter extends Character { + castSpell() { return `${this.name} casts!` } + } + // Can't have a character that BOTH flies AND casts! + ``` + </Tab> + <Tab title="Composition Approach"> + ```javascript + // Flexible behaviors — mix and match! + const canFly = (state) => ({ + fly() { return `${state.name} soars through the sky!` } + }) + + const canCast = (state) => ({ + castSpell(spell) { + return `${state.name} casts ${spell}!` + } + }) + + const canFight = (state) => ({ + attack() { return `${state.name} attacks!` } + }) + + // Create a flying mage — compose the behaviors you need! + function createFlyingMage(name) { + const state = { name, health: 100, mana: 50 } + return { + ...state, + ...canFly(state), + ...canCast(state), + ...canFight(state) + } + } + + const merlin = createFlyingMage("Merlin") + console.log(merlin.fly()) // "Merlin soars through the sky!" + console.log(merlin.castSpell("Ice")) // "Merlin casts Ice!" + console.log(merlin.attack()) // "Merlin attacks!" + ``` + </Tab> +</Tabs> + +### When to Use Each + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ INHERITANCE vs COMPOSITION │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Use INHERITANCE when: Use COMPOSITION when: │ +│ ───────────────────── ──────────────────── │ +│ │ +│ • Clear "IS-A" relationship • "HAS-A" relationship │ +│ (Warrior IS-A Character) (Character HAS inventory) │ +│ │ +│ • Child uses MOST of parent's • Only need SOME behaviors │ +│ functionality │ +│ │ +│ • Hierarchy is shallow • Behaviors need to be mixed │ +│ (2-3 levels max) freely │ +│ │ +│ • Relationships are stable • Requirements change frequently │ +│ and unlikely to change │ +│ │ +│ • You control the parent class • Inheriting from 3rd party code │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +<Warning> +**The Rule of Thumb:** "Favor composition over inheritance." Start with composition. Only use inheritance when you have a clear, stable "IS-A" relationship and the child truly needs most of the parent's behavior. +</Warning> + +--- + +## Mixins: Sharing Behavior Without Inheritance + +**Mixins** provide a way to add functionality to classes without using inheritance. They're like a toolkit of behaviors you can "mix in" to any class. + +### Basic Mixin Pattern + +```javascript +// Define behaviors as objects +const Swimmer = { + swim() { + return `${this.name} swims through the water!` + } +} + +const Flyer = { + fly() { + return `${this.name} soars through the sky!` + } +} + +const Walker = { + walk() { + return `${this.name} walks on land!` + } +} + +// A base class +class Animal { + constructor(name) { + this.name = name + } +} + +// Mix behaviors into classes as needed +class Duck extends Animal { } +Object.assign(Duck.prototype, Swimmer, Flyer, Walker) + +class Fish extends Animal { } +Object.assign(Fish.prototype, Swimmer) + +class Eagle extends Animal { } +Object.assign(Eagle.prototype, Flyer, Walker) + +// Use them! +const donald = new Duck("Donald") +console.log(donald.swim()) // "Donald swims through the water!" +console.log(donald.fly()) // "Donald soars through the sky!" +console.log(donald.walk()) // "Donald walks on land!" + +const nemo = new Fish("Nemo") +console.log(nemo.swim()) // "Nemo swims through the water!" +// nemo.fly() // ❌ Error: fly is not a function +``` + +### Functional Mixin Pattern + +A cleaner approach uses functions that take a class and return an enhanced class: + +```javascript +// Mixins as functions that enhance classes +const withLogging = (Base) => class extends Base { + log(message) { + console.log(`[${this.name}]: ${message}`) + } +} + +const withTimestamp = (Base) => class extends Base { + getTimestamp() { + return new Date().toISOString() + } +} + +// Apply mixins by wrapping the class +class Character { + constructor(name) { + this.name = name + } +} + +// Stack multiple mixins! +class LoggedCharacter extends withTimestamp(withLogging(Character)) { + doAction() { + this.log(`Action performed at ${this.getTimestamp()}`) + } +} + +const hero = new LoggedCharacter("Aragorn") +hero.doAction() // "[Aragorn]: Action performed at 2024-01-15T..." +``` + +### When to Use Mixins + +| Use Case | Example | +|----------|---------| +| Cross-cutting concerns | Logging, serialization, event handling | +| Multiple behaviors needed | A class that needs swimming AND flying | +| Third-party class extension | Adding methods to classes you don't control | +| Avoiding deep hierarchies | Instead of `FlyingSwimmingWalkingAnimal` | + +<Warning> +**Mixin Gotchas:** +- **Name collisions**: If two mixins define the same method, one overwrites the other +- **"this" confusion**: Mixins must work with whatever `this` they're mixed into +- **Hidden dependencies**: Mixins might expect certain properties to exist +- **Debugging difficulty**: Hard to trace where methods come from +</Warning> + +--- + +## Common Mistakes + +### 1. Forgetting to Call `super()` in Constructor + +```javascript +// ❌ WRONG — ReferenceError! +class Warrior extends Character { + constructor(name) { + this.rage = 0 // Error: must call super first! + super(name) + } +} + +// ✓ CORRECT — super() first, always +class Warrior extends Character { + constructor(name) { + super(name) // FIRST! + this.rage = 0 // Now this is safe + } +} +``` + +### 2. Using `this` Before `super()` + +```javascript +// ❌ WRONG — Can't use 'this' until super() is called +class Mage extends Character { + constructor(name, mana) { + this.mana = mana // ReferenceError! + super(name) + } +} + +// ✓ CORRECT +class Mage extends Character { + constructor(name, mana) { + super(name) + this.mana = mana // Works now! + } +} +``` + +### 3. Deep Inheritance Hierarchies + +```javascript +// ❌ BAD — Too deep, too fragile +class Entity { } +class LivingEntity extends Entity { } +class Animal extends LivingEntity { } +class Mammal extends Animal { } +class Canine extends Mammal { } +class Dog extends Canine { } +class Labrador extends Dog { } // 7 levels deep! 😱 + +// ✓ BETTER — Keep it shallow, use composition +class Dog { + constructor(breed) { + this.breed = breed + this.behaviors = { + ...canWalk, + ...canBark, + ...canFetch + } + } +} +``` + +### 4. Inheriting Just for Code Reuse + +```javascript +// ❌ WRONG — Stack is NOT an Array (violates IS-A) +class Stack extends Array { + peek() { return this[this.length - 1] } +} + +const stack = new Stack() +stack.push(1, 2, 3) +stack.shift() // 😱 Stacks shouldn't allow this! + +// ✓ CORRECT — Stack HAS-A array (composition) +class Stack { + #items = [] + + push(item) { this.#items.push(item) } + pop() { return this.#items.pop() } + peek() { return this.#items[this.#items.length - 1] } +} +``` + +### Inheritance Decision Flowchart + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ SHOULD I USE INHERITANCE? │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Is it an "IS-A" relationship? │ +│ (A Warrior IS-A Character?) │ +│ │ │ +│ YES │ NO │ +│ │ └──────► Use COMPOSITION ("HAS-A") │ +│ ▼ │ +│ Will child use MOST of parent's methods? │ +│ │ │ +│ YES │ NO │ +│ │ └──────► Use COMPOSITION or MIXINS │ +│ ▼ │ +│ Is hierarchy shallow (≤3 levels)? │ +│ │ │ +│ YES │ NO │ +│ │ └──────► REFACTOR! Flatten with composition │ +│ ▼ │ +│ Use INHERITANCE ✓ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Classic Interview Questions + +<AccordionGroup> + <Accordion title="What's the difference between inheritance and composition?"> + **Inheritance** establishes an "IS-A" relationship where a child class inherits all properties and methods from a parent class. It creates a tight coupling between classes. + + **Composition** establishes a "HAS-A" relationship where a class contains instances of other classes to reuse their functionality. It provides more flexibility and loose coupling. + + ```javascript + // Inheritance: Warrior IS-A Character + class Warrior extends Character { } + + // Composition: Character HAS-A weapon + class Character { + constructor() { + this.weapon = new Sword() // HAS-A + } + } + ``` + + **Rule of thumb:** Favor composition for flexibility, use inheritance for true type hierarchies. + </Accordion> + + <Accordion title="Explain polymorphism with an example"> + **Polymorphism** means "many forms" — the ability for different objects to respond to the same method call in different ways. + + ```javascript + class Shape { + area() { return 0 } + } + + class Rectangle extends Shape { + constructor(w, h) { super(); this.w = w; this.h = h } + area() { return this.w * this.h } + } + + class Circle extends Shape { + constructor(r) { super(); this.r = r } + area() { return Math.PI * this.r ** 2 } + } + + // Polymorphism in action — same method, different results + const shapes = [new Rectangle(4, 5), new Circle(3)] + shapes.forEach(s => console.log(s.area())) + // 20 + // 28.274... + ``` + + The `area()` method works differently based on the actual object type, but we can treat all shapes uniformly. + </Accordion> + + <Accordion title="What does the 'super' keyword do in JavaScript?"> + `super` has two main uses: + + 1. **`super()`** — Calls the parent class constructor (required in child constructors before using `this`) + 2. **`super.method()`** — Calls a method from the parent class + + ```javascript + class Parent { + constructor(name) { this.name = name } + greet() { return `Hello, I'm ${this.name}` } + } + + class Child extends Parent { + constructor(name, age) { + super(name) // Call parent constructor + this.age = age + } + + greet() { + return `${super.greet()} and I'm ${this.age}` // Call parent method + } + } + ``` + </Accordion> + + <Accordion title="Why might deep inheritance hierarchies be problematic?"> + Deep hierarchies (more than 3 levels) create several problems: + + 1. **Fragile Base Class Problem**: Changes to a parent class can break many descendants + 2. **Tight Coupling**: Child classes become dependent on implementation details + 3. **Inflexibility**: Hard to reuse code outside the hierarchy + 4. **Complexity**: Difficult to understand and debug method resolution + 5. **The Gorilla-Banana Problem**: You inherit everything, even what you don't need + + **Solution:** Keep hierarchies shallow (2-3 levels max) and prefer composition for sharing behavior. + </Accordion> + + <Accordion title="How does JavaScript inheritance differ from classical OOP languages?"> + JavaScript uses **prototype-based inheritance** rather than class-based: + + | Classical OOP (Java, C++) | JavaScript | + |---------------------------|------------| + | Classes are blueprints | "Classes" are functions with prototypes | + | Objects are instances of classes | Objects inherit from other objects | + | Static class hierarchy | Dynamic prototype chain | + | Multiple inheritance via interfaces | Single prototype chain (use mixins for multiple) | + + ES6 `class` syntax is syntactic sugar — under the hood, it's still prototypes: + + ```javascript + class Dog extends Animal { } + + // Is equivalent to setting up: + // Dog.prototype.__proto__ === Animal.prototype + ``` + </Accordion> +</AccordionGroup> + +--- + +## Key Takeaways + +<Info> +**Remember these essential points about Inheritance & Polymorphism:** + +1. **Inheritance lets child classes reuse parent code** — use `extends` to create class hierarchies + +2. **Always call `super()` first in child constructors** — before using `this` + +3. **`super.method()` calls the parent's version** — useful for extending rather than replacing behavior + +4. **Method overriding = same name, different behavior** — the child's method shadows the parent's + +5. **Polymorphism = "many forms"** — treat different object types through a common interface + +6. **ES6 classes are syntactic sugar over prototypes** — understand prototypes for debugging + +7. **"IS-A" → inheritance, "HAS-A" → composition** — use the right tool for the relationship + +8. **The Gorilla-Banana problem is real** — deep hierarchies inherit too much baggage + +9. **Favor composition over inheritance** — it's more flexible and maintainable + +10. **Keep inheritance hierarchies shallow** — 2-3 levels maximum + +11. **Mixins share behavior without inheritance chains** — useful for cross-cutting concerns + +12. **`instanceof` checks the entire prototype chain** — `warrior instanceof Character` is `true` +</Info> + +--- + +## Test Your Knowledge + +<AccordionGroup> + <Accordion title="1. What happens if you forget to call super() in a child constructor?"> + **Answer:** JavaScript throws a `ReferenceError` with the message "Must call super constructor in derived class before accessing 'this' or returning from derived constructor". + + ```javascript + class Child extends Parent { + constructor() { + this.name = "test" // ❌ ReferenceError! + } + } + ``` + + The `super()` call is mandatory because it initializes the parent part of the object, which must happen before the child can add its own properties. + </Accordion> + + <Accordion title="2. How does method overriding enable polymorphism?"> + **Answer:** Method overriding allows different classes to provide their own implementation of the same method name. This enables polymorphism because code can call that method on any object without knowing its specific type — each object responds appropriately. + + ```javascript + function makeSound(animal) { + console.log(animal.speak()) // Works with ANY animal type + } + + class Dog { speak() { return "Woof!" } } + class Cat { speak() { return "Meow!" } } + + makeSound(new Dog()) // "Woof!" + makeSound(new Cat()) // "Meow!" + ``` + </Accordion> + + <Accordion title="3. When should you prefer composition over inheritance?"> + **Answer:** Prefer composition when: + + - The relationship is "HAS-A" rather than "IS-A" + - You only need some of the parent's functionality + - Behaviors need to be mixed freely (e.g., flying + swimming) + - Requirements change frequently + - You're working with third-party code you don't control + - The inheritance hierarchy would exceed 3 levels + + ```javascript + // Use composition: Character HAS abilities + class Character { + constructor() { + this.abilities = [canAttack, canDefend, canHeal] + } + } + ``` + </Accordion> + + <Accordion title="4. What's a mixin and when would you use one?"> + **Answer:** A mixin is a way to add functionality to classes without using inheritance. It's an object (or function) containing methods that can be "mixed into" multiple classes. + + Use mixins for: + - Cross-cutting concerns (logging, serialization) + - When a class needs behaviors from multiple sources + - Avoiding the diamond problem of multiple inheritance + + ```javascript + const Serializable = { + toJSON() { return JSON.stringify(this) } + } + + class User { constructor(name) { this.name = name } } + Object.assign(User.prototype, Serializable) + + new User("Alice").toJSON() // '{"name":"Alice"}' + ``` + </Accordion> + + <Accordion title="5. How can you call a parent's method from an overriding method?"> + **Answer:** Use `super.methodName()` to call the parent's version of an overridden method: + + ```javascript + class Parent { + greet() { return "Hello" } + } + + class Child extends Parent { + greet() { + const parentGreeting = super.greet() // "Hello" + return `${parentGreeting} from Child!` + } + } + + new Child().greet() // "Hello from Child!" + ``` + + This is useful when you want to extend behavior rather than completely replace it. + </Accordion> + + <Accordion title="6. What's the 'IS-A' test for inheritance?"> + **Answer:** The "IS-A" test determines if inheritance is appropriate by asking: "Is the child truly a specialized type of the parent?" + + - **Passes:** "A Warrior IS-A Character" ✓ + - **Passes:** "A Dog IS-A Animal" ✓ + - **Fails:** "A Stack IS-A Array" ✗ (Stack has different behavior) + - **Fails:** "A Car IS-A Engine" ✗ (Car HAS-A Engine) + + If it fails the IS-A test, use composition instead. This prevents the Liskov Substitution Principle violations where child instances can't properly substitute for parent instances. + </Accordion> +</AccordionGroup> + +--- + +## Related Concepts + +<CardGroup cols={2}> + <Card title="Factories & Classes" icon="hammer" href="/concepts/factories-classes"> + Learn the fundamentals of creating objects with factory functions and ES6 classes + </Card> + <Card title="Object Creation & Prototypes" icon="sitemap" href="/concepts/object-creation-prototypes"> + Understand the prototype chain that powers JavaScript inheritance + </Card> + <Card title="this, call, apply, bind" icon="bullseye" href="/concepts/this-call-apply-bind"> + Master context binding — essential for understanding method inheritance + </Card> + <Card title="Design Patterns" icon="compass-drafting" href="/concepts/design-patterns"> + Learn patterns like Strategy and Decorator that use polymorphism + </Card> +</CardGroup> + +--- + +## Reference + +<CardGroup cols={2}> + <Card title="Classes — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes"> + Complete guide to ES6 classes in JavaScript + </Card> + <Card title="extends — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/extends"> + Official documentation for the extends keyword + </Card> + <Card title="super — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/super"> + How to use super for parent class access + </Card> + <Card title="Inheritance and the prototype chain — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain"> + Deep dive into how inheritance really works in JavaScript + </Card> +</CardGroup> + +## Articles + +<CardGroup cols={2}> + <Card title="Class Inheritance — JavaScript.info" icon="newspaper" href="https://javascript.info/class-inheritance"> + A comprehensive guide to class inheritance with extends and super + </Card> + <Card title="Understanding Classes in JavaScript — DigitalOcean" icon="newspaper" href="https://www.digitalocean.com/community/tutorials/understanding-classes-in-javascript"> + Deep exploration of ES6 class syntax and OOP principles + </Card> + + <Card title="The Gorilla-Banana Problem" icon="newspaper" href="https://www.johndcook.com/blog/2011/07/19/you-wanted-banana/"> + Joe Armstrong's famous OOP criticism from "Coders at Work" — you wanted a banana but got the whole jungle + </Card> +</CardGroup> + +## Videos + +<CardGroup cols={2}> + <Card title="JavaScript ES6 Classes and Inheritance" icon="video" href="https://www.youtube.com/watch?v=RBLIm5LMrmc"> + Traversy Media's ES6 series covers class syntax and inheritance with clear, practical examples + </Card> + <Card title="Inheritance in JavaScript" icon="video" href="https://www.youtube.com/watch?v=yXlFR81tDBM"> + Detailed walkthrough of inheritance concepts by kudvenkat + </Card> + <Card title="Composition over Inheritance" icon="video" href="https://www.youtube.com/watch?v=wfMtDGfHWpA"> + Fun Fun Function explains why composition is often better + </Card> + <Card title="Polymorphism in JavaScript" icon="video" href="https://www.youtube.com/watch?v=zdovG9cuEBA"> + Clear explanation of polymorphism with practical examples + </Card> +</CardGroup> diff --git a/docs/concepts/javascript-engines.mdx b/docs/concepts/javascript-engines.mdx new file mode 100644 index 00000000..219523d8 --- /dev/null +++ b/docs/concepts/javascript-engines.mdx @@ -0,0 +1,941 @@ +--- +title: "JavaScript Engines: How V8 Runs Your Code in JavaScript" +sidebarTitle: "JavaScript Engines: How V8 Runs Your Code" +description: "Learn how JavaScript engines work. Understand V8's architecture, parsing, compilation, JIT optimization, hidden classes, inline caching, and garbage collection." +--- + +What happens when you run JavaScript code? How does a browser turn `const x = 1 + 2` into something your computer actually executes? When you write a function, what transforms those characters into instructions your CPU understands? + +```javascript +function greet(name) { + return "Hello, " + name + "!" +} + +greet("World") // "Hello, World!" +``` + +Behind every line of JavaScript is a **JavaScript engine**. It's the program that reads your code, understands it, and makes it run. The most popular engine is **[V8](https://v8.dev/)**, which powers Chrome, Node.js, Deno, and Electron. Understanding how V8 works helps you write faster code and debug performance issues. + +<Info> +**What you'll learn in this guide:** +- What a JavaScript engine is and what it does +- How V8 parses your code and builds an Abstract Syntax Tree +- How Ignition (interpreter) and TurboFan (compiler) work together +- What JIT compilation is and why it makes JavaScript fast +- How hidden classes and inline caching optimize property access +- How garbage collection automatically manages memory +- Practical tips for writing engine-friendly code +</Info> + +<Warning> +**Prerequisite:** This guide assumes you're comfortable with basic JavaScript syntax. Some concepts connect to the [Call Stack](/concepts/call-stack) and [Event Loop](/concepts/event-loop), so reading those first helps! +</Warning> + +--- + +## What is a JavaScript Engine? + +A **JavaScript engine** is a program that executes JavaScript code. It takes the source code you write and converts it into machine code that your computer's processor can run. + +Every browser has its own JavaScript engine: + +| Browser | Engine | Also Used By | +|---------|--------|--------------| +| Chrome | **V8** | Node.js, Deno, Electron | +| Firefox | SpiderMonkey | — | +| Safari | JavaScriptCore | Bun | +| Edge | V8 (since 2020) | — | + +We'll focus on **V8** since it's the most widely used engine and powers both browser and server-side JavaScript. + +<Note> +All JavaScript engines implement the [ECMAScript specification](https://tc39.es/ecma262/), which defines how the language should work. That's why JavaScript behaves the same way whether you run it in Chrome, Firefox, or Node.js. +</Note> + +--- + +## How Does a JavaScript Engine Work? + +Think of V8 as a **factory** that manufactures results from your code: + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ THE V8 JAVASCRIPT FACTORY │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ RAW MATERIALS QUALITY CONTROL BLUEPRINT │ +│ (Source Code) (Parser) (AST) │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ function │ │ Break into │ │ Tree of │ │ +│ │ add(a, b) { │ ─► │ tokens, │ ─► │ operations │ │ +│ │ return a+b │ │ check │ │ to perform │ │ +│ │ } │ │ syntax │ │ │ │ +│ └──────────────┘ └──────────────┘ └──────┬───────┘ │ +│ │ │ +│ ▼ │ +│ ┌───────────────────────────────────────────────────────────────────┐ │ +│ │ ASSEMBLY LINE │ │ +│ │ ┌─────────────────┐ ┌─────────────────────────┐ │ │ +│ │ │ IGNITION │ │ TURBOFAN │ │ │ +│ │ │ (Interpreter) │ ─────────► │ (Optimizing Compiler) │ │ │ +│ │ │ │ "hot" │ │ │ │ +│ │ │ Steady workers │ code │ Fast robotic assembly │ │ │ +│ │ │ Start quickly │ │ Takes time to set up │ │ │ +│ │ └─────────────────┘ └─────────────────────────┘ │ │ +│ └───────────────────────────────────────────────────────────────────┘ │ +│ │ +│ ▼ │ +│ ┌──────────────┐ │ +│ │ OUTPUT │ │ +│ │ (Result) │ │ +│ └──────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +Here's the analogy: + +- **Raw materials (source code)**: Your JavaScript files come in as text +- **Quality control (parser)**: Checks for syntax errors, breaks code into pieces +- **Blueprint (AST)**: A structured representation of what needs to be built +- **Assembly line workers (Ignition)**: Start working immediately, steady pace +- **Robotic automation (TurboFan)**: Takes time to set up, but once running, it's much faster + +Just like a factory might start with manual workers and add robots for repetitive tasks, V8 starts interpreting code immediately, then optimizes the parts that run frequently. + +--- + +## How Does V8 Execute Your Code? + +When you run JavaScript, V8 processes your code through several stages. Let's trace through what happens when V8 executes this code: + +```javascript +function add(a, b) { + return a + b +} + +add(1, 2) // 3 +``` + +### Step 1: Parsing + +First, V8 needs to understand your code. The **parser** reads the source text and converts it into a structured format. + +<Steps> + <Step title="Tokenization (Lexical Analysis)"> + The code is broken into **tokens**, the smallest meaningful pieces: + + ``` + 'function' 'add' '(' 'a' ',' 'b' ')' '{' 'return' 'a' '+' 'b' '}' + ``` + + Each token is classified: `function` is a keyword, `add` is an identifier, `+` is an operator. + </Step> + + <Step title="Building the AST (Syntactic Analysis)"> + Tokens are organized into an **Abstract Syntax Tree (AST)**, a tree structure that represents your code's meaning: + + ``` + FunctionDeclaration + ├── name: "add" + ├── params: ["a", "b"] + └── body: ReturnStatement + └── BinaryExpression + ├── left: Identifier "a" + ├── operator: "+" + └── right: Identifier "b" + ``` + + The AST captures *what* your code does, without the original syntax (semicolons, whitespace, etc.). + </Step> +</Steps> + +<Tip> +**See it yourself:** You can explore how JavaScript is parsed using [AST Explorer](https://astexplorer.net/). Paste any JavaScript code and see the resulting tree structure. +</Tip> + +### Step 2: Ignition (The Interpreter) + +Once V8 has the AST, **Ignition** takes over. Ignition is V8's interpreter. It walks through the AST and generates **bytecode**, a compact representation of your code. + +``` +Bytecode for add(a, b): + Ldar a1 // Load argument 'a' into accumulator + Add a2 // Add argument 'b' to accumulator + Return // Return the accumulator value +``` + +Ignition then **executes** this bytecode immediately. No waiting around for optimization. Your code starts running right away. + +While executing, Ignition also collects **profiling data**: +- Which functions are called often? +- What types of values does each variable hold? +- Which branches of if/else statements are taken? + +This profiling data becomes important for the next step. + +### Step 3: TurboFan (The Optimizing Compiler) + +When Ignition notices a function is called many times (it becomes "hot"), V8 decides it's worth spending time to optimize it. Enter **TurboFan**, V8's optimizing compiler. + +TurboFan takes the bytecode and profiling data, then generates **highly optimized machine code**. It makes assumptions based on the profiling data: + +```javascript +function add(a, b) { + return a + b +} + +// V8 observes: add() is always called with numbers +add(1, 2) +add(3, 4) +add(5, 6) +// ... called many more times with numbers + +// TurboFan thinks: "This always gets numbers. I'll optimize for that!" +// Generates machine code that assumes a and b are numbers +``` + +The optimized code runs **much faster** than interpreted bytecode because: +- It's native machine code, not bytecode that needs interpretation +- It makes type assumptions (no need to check "is this a number?" every time) +- It can inline function calls, eliminate dead code, and apply other optimizations + +### Step 4: Deoptimization (The Fallback) + +But what if TurboFan's assumptions are wrong? + +```javascript +// After 1000 calls with numbers... +add("hello", "world") // Strings! TurboFan assumed numbers! +``` + +When this happens, V8 performs **deoptimization**. It throws away the optimized machine code and falls back to Ignition's bytecode. The function runs slower temporarily, but at least it runs correctly. + +V8 might try to optimize again later, this time with better information about the actual types being used. + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ THE OPTIMIZATION CYCLE │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Source Code │ +│ │ │ +│ ▼ │ +│ ┌─────────┐ │ +│ │ Parse │ │ +│ └────┬────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────┐ profile ┌───────────┐ │ +│ │ Ignition │ ───────────────────► │ TurboFan │ │ +│ │(bytecode)│ │(optimized)│ │ +│ └────┬────┘ ◄─────────────────── └─────┬─────┘ │ +│ │ deoptimize │ │ +│ │ │ │ +│ ▼ ▼ │ +│ [Execute] [Execute] │ +│ (slower) (faster!) │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## What is JIT Compilation? + +You might have heard that JavaScript is an "interpreted language." That's only half the story. Modern JavaScript engines use **JIT compilation** (Just-In-Time), which combines interpretation and compilation. + +### The Three Approaches + +<Tabs> + <Tab title="Interpreted"> + **Pure Interpretation** (like old JavaScript engines) + + - Source code is executed line by line + - No compilation step + - Starts fast, but runs slow + - Every time a function runs, it's re-interpreted + + ``` + Source → Execute → Execute → Execute... + ``` + </Tab> + + <Tab title="Compiled"> + **Ahead-of-Time Compilation** (like C/C++) + + - Source code is compiled to machine code before running + - Slow startup (must compile everything first) + - Very fast execution + - Can't adapt to runtime information + + ``` + Source → Compile (wait...) → Execute (fast!) + ``` + </Tab> + + <Tab title="JIT (V8)"> + **Just-In-Time Compilation** (V8's approach) + + - Start executing immediately with interpreter + - Compile "hot" code to machine code while running + - Best of both worlds: fast startup AND fast execution + - Can use runtime information for smarter optimizations + + ``` + Source → Interpret (start fast!) → Compile hot code → Execute (faster!) + ``` + </Tab> +</Tabs> + +### Why JavaScript Needs JIT + +JavaScript is a **dynamic language**. Variables can hold any type, objects can change shape, and functions can be redefined at runtime. This makes ahead-of-time compilation difficult because the compiler doesn't know what types to expect. + +```javascript +function process(x) { + return x.value * 2 +} + +// x could be anything! +process({ value: 10 }) // Object with number +process({ value: "hello" }) // Object with string (NaN result) +process({ value: 10, extra: 5 }) // Different shape +``` + +JIT compilation solves this by: +1. Starting with interpretation (works for any types) +2. Observing what types actually appear at runtime +3. Compiling optimized code based on real observations +4. Falling back to interpretation if observations were wrong + +<Warning> +**The "warm-up" period:** When you first run JavaScript code, it's slower because it's being interpreted. After functions run many times, they get optimized and become faster. This is why benchmarks often include a "warm-up" phase. +</Warning> + +--- + +## What Are Hidden Classes? + +**Hidden classes** (called "Maps" in V8, "Shapes" in other engines) are internal data structures that V8 uses to track object shapes. They let V8 know exactly where to find properties like `obj.x` without searching through every property name. + +Why does V8 need them? JavaScript objects are dynamic. You can add or remove properties at any time. This flexibility creates a problem: how does V8 efficiently access `obj.x` if objects can have any shape? + +### The Problem + +Consider accessing a property: + +```javascript +function getX(obj) { + return obj.x +} +``` + +Without optimization, every call to `getX` would need to: +1. Look up the object's list of properties +2. Search for a property named "x" +3. Get the value at that property's location + +That's slow, especially for hot code. + +### The Solution: Hidden Classes + +V8 assigns a **hidden class** to every object. Objects with the same properties in the same order share the same hidden class. + +```javascript +const point1 = { x: 1, y: 2 } +const point2 = { x: 5, y: 10 } + +// point1 and point2 have the SAME hidden class! +// V8 knows: "For objects with this hidden class, 'x' is at offset 0, 'y' is at offset 1" +``` + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ HIDDEN CLASSES │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Hidden Class HC1 point1 point2 │ +│ ┌────────────────────┐ ┌────────┐ ┌────────┐ │ +│ │ x: offset 0 │ ◄────── │ HC1 │ │ HC1 │ ◄──┐ │ +│ │ y: offset 1 │ ├────────┤ ├────────┤ │ │ +│ └────────────────────┘ │ [0]: 1 │ │ [0]: 5 │ │ │ +│ ▲ │ [1]: 2 │ │ [1]: 10│ │ │ +│ │ └────────┘ └────────┘ │ │ +│ │ │ │ +│ └───────────────────── Same hidden class! ──────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +Now, when V8 sees `getX(point1)`, it can: +1. Check the hidden class (one comparison) +2. Read the value at offset 0 (direct memory access) + +No property name lookup needed! + +### Transition Chains + +What happens when you add properties to an object? V8 creates **transition chains**: + +```javascript +const obj = {} // Hidden class: HC0 (empty) +obj.x = 1 // Transition to HC1 (has x at offset 0) +obj.y = 2 // Transition to HC2 (has x at 0, y at 1) +``` + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ TRANSITION CHAIN │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ const obj = {} obj.x = 1 obj.y = 2 │ +│ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ HC0 │ ───► │ HC1 │ ───► │ HC2 │ │ +│ │ (empty) │ add x │ x: off 0 │ add y │ x: off 0 │ │ +│ └──────────┘ └──────────┘ │ y: off 1 │ │ +│ └──────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +<Warning> +**Property order matters!** These two objects have **different** hidden classes: + +```javascript +const a = { x: 1, y: 2 } // HC with x then y +const b = { y: 2, x: 1 } // Different HC with y then x +``` + +This means V8 can't share optimizations between them. Always add properties in the same order! +</Warning> + +--- + +## What is Inline Caching? + +**Inline Caching (IC)** is an optimization where V8 remembers where it found a property and reuses that information on subsequent calls. Instead of looking up property locations every time, V8 caches: "For this hidden class, property X is at memory offset Y." + +This optimization is possible because of hidden classes. When V8 knows an object's shape, it can cache the exact memory location of each property. + +### How Inline Caching Works + +```javascript +function getX(obj) { + return obj.x // V8 caches: "For HC1, x is at offset 0" +} + +const p1 = { x: 1, y: 2 } +const p2 = { x: 5, y: 10 } + +getX(p1) // First call: look up x, cache the location +getX(p2) // Second call: same hidden class! Use cached location +getX(p1) // Third call: cache hit again! +``` + +The first time `getX` runs, V8 does the full property lookup. But it **caches** the result: "For objects with hidden class HC1, property 'x' is at memory offset 0." + +Subsequent calls with the same hidden class skip the lookup entirely. + +### IC States: Monomorphic, Polymorphic, Megamorphic + +The inline cache can be in different states depending on how many different hidden classes it encounters: + +<AccordionGroup> + <Accordion title="Monomorphic (Fastest)"> + The function always sees objects with the **same** hidden class. + + ```javascript + function getX(obj) { + return obj.x + } + + // All objects have the same shape + getX({ x: 1, y: 2 }) + getX({ x: 3, y: 4 }) + getX({ x: 5, y: 6 }) + + // IC: "Always HC1, x at offset 0" - ONE entry, super fast! + ``` + + **Performance:** Excellent. Single comparison, direct memory access. + </Accordion> + + <Accordion title="Polymorphic (Still Good)"> + The function sees a **few** different hidden classes (typically 2-4). + + ```javascript + function getX(obj) { + return obj.x + } + + getX({ x: 1 }) // Shape A + getX({ x: 2, y: 3 }) // Shape B + getX({ x: 4, y: 5, z: 6 }) // Shape C + + // IC: "Could be A, B, or C" - checks a few options + ``` + + **Performance:** Good. Checks a small list of known shapes. + </Accordion> + + <Accordion title="Megamorphic (Slowest)"> + The function sees **many** different hidden classes. + + ```javascript + function getX(obj) { + return obj.x + } + + // Every call has a completely different shape + getX({ x: 1 }) + getX({ x: 2, a: 1 }) + getX({ x: 3, b: 2 }) + getX({ x: 4, c: 3 }) + getX({ x: 5, d: 4 }) + // ... many more different shapes + + // IC gives up: "Too many shapes, doing full lookup every time" + ``` + + **Performance:** Poor. Falls back to generic property lookup. + </Accordion> +</AccordionGroup> + +<Tip> +**For best performance:** Pass objects with consistent shapes to your functions. Factory functions help: + +```javascript +// Good: Factory creates consistent shapes +function createPoint(x, y) { + return { x, y } +} + +getX(createPoint(1, 2)) +getX(createPoint(3, 4)) // Same shape, monomorphic IC! +``` +</Tip> + +--- + +## How Does Garbage Collection Work? + +Unlike languages like C where you manually allocate and free memory, JavaScript automatically manages memory through **garbage collection (GC)**. V8's garbage collector is called **Orinoco**. + +### The Generational Hypothesis + +V8's GC is based on an observation about how programs use memory: **most objects die young**. + +Think about it: temporary variables, intermediate calculation results, short-lived callbacks. They're created, used briefly, and never needed again. Only some objects (your app's state, cached data) live for a long time. + +V8 exploits this by splitting memory into generations: + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ V8 MEMORY HEAP │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ YOUNG GENERATION OLD GENERATION │ +│ (Short-lived objects) (Long-lived objects) │ +│ │ +│ ┌─────────────────────────┐ ┌─────────────────────────┐ │ +│ │ Nursery │ Intermediate │ ───► │ Survived multiple GCs │ │ +│ │ │ │ survives │ │ │ +│ │ New │ Survived │ │ App state, caches, │ │ +│ │ objects │ one GC │ │ long-lived data │ │ +│ └─────────────────────────┘ └─────────────────────────┘ │ +│ │ +│ Minor GC (Scavenger) Major GC (Mark-Compact) │ +│ • Very fast • Slower but thorough │ +│ • Runs frequently • Runs less often │ +│ • Only scans young gen • Scans entire heap │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### Minor GC: The Scavenger + +New objects are allocated in the **young generation**. When it fills up, V8 runs a **minor GC** (called the Scavenger): + +1. Find all live objects in the young generation +2. Copy survivors to a new space +3. Objects that survive multiple collections get promoted to the old generation + +This is fast because: +- Most young objects are dead (no need to copy them) +- The young generation is small +- Only copying live objects means no fragmentation + +### Major GC: Mark-Compact + +The **old generation** is collected less frequently with a **major GC**: + +<Steps> + <Step title="Marking"> + Starting from "roots" (global variables, stack), V8 follows all references and marks every reachable object as "live." + </Step> + + <Step title="Sweeping"> + Dead objects (unmarked) leave gaps in memory. V8 adds these gaps to a "free list" for future allocations. + </Step> + + <Step title="Compaction"> + To reduce fragmentation, V8 may move live objects together, like defragmenting a hard drive. + </Step> +</Steps> + +### Concurrent and Parallel GC + +Modern V8 uses advanced techniques to minimize pauses: + +- **Parallel:** Multiple threads do GC work simultaneously +- **Incremental:** GC work is broken into small chunks, interleaved with JavaScript execution +- **Concurrent:** GC runs in the background while JavaScript continues executing + +This means you rarely notice GC pauses in modern JavaScript applications. + +--- + +## How Do You Write Engine-Friendly Code? + +Now that you understand how V8 works, here are practical tips to help the engine optimize your code: + +### 1. Initialize Objects Consistently + +Give objects the same shape by adding properties in the same order: + +```javascript +// ✓ Good: Consistent shape +function createUser(name, age) { + return { name, age } // Always name, then age +} + +// ❌ Bad: Inconsistent shapes +function createUser(name, age) { + const user = {} + if (name) user.name = name // Sometimes name first + if (age) user.age = age // Sometimes age first + return user +} +``` + +### 2. Avoid Changing Types + +Keep variables holding the same type throughout their lifetime: + +```javascript +// ✓ Good: Consistent types +let count = 0 +count = 1 +count = 2 + +// ❌ Bad: Type changes trigger deoptimization +let count = 0 +count = "none" // Now it's a string! +count = null // Now it's null! +``` + +### 3. Use Arrays Correctly + +Avoid "holes" in arrays and don't mix types: + +```javascript +// ✓ Good: Dense array with consistent types +const numbers = [1, 2, 3, 4, 5] + +// ❌ Bad: Sparse array with holes +const sparse = [] +sparse[0] = 1 +sparse[1000] = 2 // Creates 999 "holes" + +// ❌ Bad: Mixed types +const mixed = [1, "two", 3, null, { four: 4 }] +``` + +### 4. Avoid [`delete`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete) on Objects + +Using `delete` changes an object's hidden class and can cause deoptimization: + +```javascript +// ❌ Bad: Using delete +const user = { name: "Alice", age: 30, temp: true } +delete user.temp // Changes hidden class! + +// ✓ Good: Set to undefined or use a different structure +const user = { name: "Alice", age: 30, temp: true } +user.temp = undefined // Hidden class stays the same +``` + +<Note> +Setting a property to `undefined` keeps the property on the object (it just has no value). If you need to truly remove properties frequently, consider using a `Map` instead of a plain object. +</Note> + +### 5. Prefer Monomorphic Code + +Design functions to work with objects of the same shape: + +```javascript +// ✓ Good: Monomorphic - always same shape +class Point { + constructor(x, y) { + this.x = x + this.y = y + } +} + +function distance(p1, p2) { + const dx = p1.x - p2.x + const dy = p1.y - p2.y + return Math.sqrt(dx * dx + dy * dy) +} + +distance(new Point(0, 0), new Point(3, 4)) // All Points, same shape +``` + +--- + +## Common Misconceptions + +<AccordionGroup> + <Accordion title="'JavaScript is interpreted, not compiled'"> + **Partially true, but misleading.** Modern JavaScript engines use JIT compilation. Your code is initially interpreted, but hot functions are compiled to native machine code. V8's TurboFan generates highly optimized machine code that rivals traditionally compiled languages for computational tasks. + </Accordion> + + <Accordion title="'More code = slower execution'"> + **Not necessarily!** V8 performs dead code elimination and function inlining. A well-structured program with more lines can be faster than a "clever" one-liner that's hard to optimize. Write clear, predictable code and let the engine optimize it. + </Accordion> + + <Accordion title="'I need to manually manage memory in JavaScript'"> + **No!** JavaScript has automatic garbage collection. You don't need to (and can't) manually free memory. However, you should avoid creating unnecessary object references that prevent garbage collection (memory leaks). + + ```javascript + // Potential memory leak: event listener keeps reference + element.addEventListener("click", () => { + console.log(largeData) // largeData can't be GC'd + }) + + // Fix: Remove listener when done + element.removeEventListener("click", handler) + ``` + </Accordion> + + <Accordion title="'eval() is just slow'"> + **It's worse than slow.** [`eval()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval) prevents many optimizations because V8 can't predict what code will run. Variables in scope become "unoptimizable" because `eval` might access them. Avoid `eval()` and [`new Function()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/Function) with dynamic strings. + </Accordion> + + <Accordion title="'typeof null === 'object' is a V8 bug'"> + **No, it's in the ECMAScript specification.** This is a historical quirk from JavaScript's original implementation that was kept for backwards compatibility. All JavaScript engines must return `"object"` for [`typeof null`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof#typeof_null) to comply with the spec. + </Accordion> +</AccordionGroup> + +--- + +## Key Takeaways + +<Info> +**The key things to remember:** + +1. **V8 powers Chrome, Node.js, and Deno.** It's the most widely used JavaScript engine and determines how your code runs. + +2. **Code goes through multiple stages:** Source → Parse → AST → Bytecode (Ignition) → Optimized Machine Code (TurboFan). + +3. **Ignition interprets immediately.** Your code starts running right away without waiting for compilation. + +4. **TurboFan optimizes hot code.** Functions called many times get compiled to fast machine code based on observed types. + +5. **Deoptimization happens when assumptions fail.** If you pass unexpected types, V8 falls back to slower bytecode. + +6. **Hidden classes enable fast property access.** Objects with the same properties in the same order share optimization metadata. + +7. **Inline caching remembers property locations.** Monomorphic code (same shapes) is fastest; megamorphic code (many shapes) is slowest. + +8. **Garbage collection is automatic and generational.** Most objects die young; V8 optimizes for this with separate young/old generations. + +9. **Write consistent, predictable code.** Same shapes, same types, dense arrays. Help the engine help you. + +10. **Avoid anti-patterns:** `delete` on objects, sparse arrays, changing variable types, and `eval()`. +</Info> + +--- + +## Test Your Knowledge + +<AccordionGroup> + <Accordion title="Question 1: What's the difference between Ignition and TurboFan?"> + **Answer:** + + **Ignition** is V8's interpreter. It generates bytecode from the AST and executes it immediately. It's fast to start but doesn't produce the fastest possible code. While running, it collects profiling data about types and execution patterns. + + **TurboFan** is V8's optimizing compiler. It takes bytecode and profiling data from Ignition, then generates highly optimized machine code. It takes longer to compile but produces much faster code. TurboFan kicks in for "hot" functions that run many times. + </Accordion> + + <Accordion title="Question 2: Why does property order matter when creating objects?"> + **Answer:** + + V8 assigns hidden classes to objects based on their properties **and the order those properties were added**. Objects with the same properties in the same order share a hidden class and can use the same optimizations. + + ```javascript + const a = { x: 1, y: 2 } // Hidden class A + const b = { y: 2, x: 1 } // Hidden class B (different!) + ``` + + Different hidden classes mean different inline cache entries and less optimization sharing. For best performance, always add properties in a consistent order. + </Accordion> + + <Accordion title="Question 3: What triggers deoptimization?"> + **Answer:** + + Deoptimization happens when TurboFan's assumptions about your code are violated. Common triggers include: + + - **Type changes:** A function optimized for numbers receives a string + - **Hidden class changes:** An object's shape changes (adding/deleting properties) + - **Unexpected values:** `undefined` where a number was expected + - **Megamorphic call sites:** Too many different object shapes at one location + + ```javascript + function add(a, b) { return a + b } + + // Optimized for numbers + add(1, 2) + add(3, 4) + + // Deoptimizes! + add("hello", "world") + ``` + </Accordion> + + <Accordion title="Question 4: What is inline caching and why does it speed up property access?"> + **Answer:** + + Inline caching (IC) is an optimization where V8 remembers where it found a property for a given hidden class. Instead of doing a full property lookup every time, it caches: "For objects with hidden class X, property 'foo' is at memory offset Y." + + On subsequent accesses with the same hidden class, V8 skips the lookup and reads directly from the cached offset. This turns an O(n) dictionary lookup into an O(1) memory access. + + ```javascript + function getX(obj) { + return obj.x // IC: "For HC1, x is at offset 0" + } + + getX({ x: 1, y: 2 }) // Cache miss, full lookup, cache result + getX({ x: 3, y: 4 }) // Cache hit! Direct access to offset 0 + ``` + </Accordion> + + <Accordion title="Question 5: What is the 'generational hypothesis' in garbage collection?"> + **Answer:** + + The generational hypothesis states that **most objects die young**. Temporary variables, function arguments, intermediate results. They're created, used briefly, and become garbage quickly. + + V8 exploits this by dividing the heap into: + - **Young generation:** Where new objects are allocated. Collected frequently with a fast "scavenger" algorithm. + - **Old generation:** Objects that survive multiple young generation collections. Collected less frequently with a slower but thorough algorithm. + + This is efficient because checking young objects frequently catches most garbage quickly, while long-lived objects aren't constantly re-checked. + </Accordion> + + <Accordion title="Question 6: Which code pattern is more engine-friendly?"> + ```javascript + // Pattern A + function createPoint(x, y) { + return { x: x, y: y } + } + + // Pattern B + function createPoint(x, y) { + const point = {} + point.x = x + point.y = y + return point + } + ``` + + **Answer:** + + **Pattern A is more engine-friendly.** + + In Pattern A, the object literal `{ x: x, y: y }` creates an object with a known shape immediately. V8 can skip the empty object transition. + + In Pattern B, the object goes through three hidden class transitions: + 1. `{}` - empty shape + 2. `{ x }` - after adding x + 3. `{ x, y }` - after adding y + + Pattern A is faster to create and produces the same final shape more directly. Modern engines optimize object literals with known properties, skipping intermediate shapes. + </Accordion> +</AccordionGroup> + +--- + +## Related Concepts + +<CardGroup cols={2}> + <Card title="Call Stack" icon="layer-group" href="/concepts/call-stack"> + How V8 tracks function execution and manages execution contexts + </Card> + <Card title="Event Loop" icon="rotate" href="/concepts/event-loop"> + How async code runs alongside the single-threaded JavaScript engine + </Card> + <Card title="Primitive Types" icon="cube" href="/concepts/primitive-types"> + How V8 represents and optimizes different value types + </Card> + <Card title="Value vs Reference Types" icon="code-branch" href="/concepts/value-reference-types"> + How the engine stores primitives vs objects in memory + </Card> +</CardGroup> + +--- + +## Reference + +<CardGroup cols={2}> + <Card title="JavaScript technologies overview — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/JavaScript_technologies_overview"> + Overview of JavaScript engines, ECMAScript, and how the language relates to browser APIs. + </Card> + <Card title="Memory Management — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management"> + How JavaScript manages memory allocation and garbage collection. + </Card> + <Card title="V8 Documentation" icon="book" href="https://v8.dev/docs"> + Official V8 documentation covering Ignition, TurboFan, and engine internals. + </Card> +</CardGroup> + +## Articles + +<CardGroup cols={2}> + <Card title="JavaScript engine fundamentals: Shapes and Inline Caches" icon="newspaper" href="https://mathiasbynens.be/notes/shapes-ics"> + Mathias Bynens and Benedikt Meurer explain how all JavaScript engines optimize property access. Includes excellent diagrams showing hidden classes and IC states. + </Card> + <Card title="JavaScript engine fundamentals: optimizing prototypes" icon="newspaper" href="https://mathiasbynens.be/notes/prototypes"> + The follow-up article covering how engines optimize prototype chain lookups. Essential reading for understanding object-oriented JavaScript performance. + </Card> + <Card title="Launching Ignition and TurboFan" icon="newspaper" href="https://v8.dev/blog/launching-ignition-and-turbofan"> + V8 team's announcement of the Ignition + TurboFan pipeline. Explains why the new architecture is faster and uses less memory. + </Card> + <Card title="Trash talk: the Orinoco garbage collector" icon="newspaper" href="https://v8.dev/blog/trash-talk"> + Deep dive into V8's modern garbage collector. Covers parallel, incremental, and concurrent techniques that minimize pause times. + </Card> + <Card title="How V8 optimizes array operations" icon="newspaper" href="https://v8.dev/blog/elements-kinds"> + V8 blog post explaining different "elements kinds" for arrays and how to write array code that V8 can optimize effectively. + </Card> + <Card title="Blazingly fast parsing, part 1: optimizing the scanner" icon="newspaper" href="https://v8.dev/blog/scanner"> + How V8 optimizes the first stage of code processing. Shows the engineering that makes JavaScript parsing fast. + </Card> +</CardGroup> + +## Videos + +<CardGroup cols={2}> + <Card title="JavaScript Engines: The Good Parts" icon="video" href="https://www.youtube.com/watch?v=5nmpokoRaZI"> + Mathias Bynens and Benedikt Meurer at JSConf EU 2018. The definitive talk on JavaScript engine internals with beautiful visualizations of shapes, ICs, and optimization. + </Card> + <Card title="JS Engine EXPOSED — Google's V8 Architecture" icon="video" href="https://www.youtube.com/watch?v=2WJL19wDH68"> + Akshay Saini's Namaste JavaScript episode on V8. Beginner-friendly explanation of parsing, compilation, and the execution pipeline. + </Card> + <Card title="Understanding the V8 JavaScript Engine" icon="video" href="https://www.youtube.com/watch?v=xckH5s3UuX4"> + freeCodeCamp talk covering V8's architecture from a Node.js perspective. Great for understanding how the engine powers server-side JavaScript. + </Card> + <Card title="A Sneak Peek Into Super Fast V8 Internals" icon="video" href="https://www.youtube.com/watch?v=wz7Znu6tqFw"> + Chrome DevSummit talk showing how V8 optimizes real-world patterns. Includes profiling examples and optimization tips. + </Card> +</CardGroup> diff --git a/docs/concepts/map-reduce-filter.mdx b/docs/concepts/map-reduce-filter.mdx new file mode 100644 index 00000000..9d31b0d1 --- /dev/null +++ b/docs/concepts/map-reduce-filter.mdx @@ -0,0 +1,1348 @@ +--- +title: "map, reduce, filter: Transform Arrays in JavaScript" +sidebarTitle: "map, reduce, filter" +description: "Learn map, reduce, and filter in JavaScript. Transform, filter, and combine arrays without mutation. Includes method chaining and common pitfalls." +--- + +How do you transform every item in an array? How do you filter out the ones you don't need? How do you combine them all into a single result? These are the three most common operations you'll perform on arrays, and JavaScript gives you three powerful methods to handle them. + +```javascript +// The power of map, filter, and reduce in action +const products = [ + { name: 'Laptop', price: 1000, inStock: true }, + { name: 'Phone', price: 500, inStock: false }, + { name: 'Tablet', price: 300, inStock: true }, + { name: 'Watch', price: 200, inStock: true } +] + +const totalInStock = products + .filter(product => product.inStock) // Keep only in-stock items + .map(product => product.price) // Extract just the prices + .reduce((sum, price) => sum + price, 0) // Sum them up + +console.log(totalInStock) // 1500 +``` + +That's **[map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map)**, **[filter](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter)**, and **[reduce](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce)** working together. Three methods that transform how you work with arrays. + +<Info> +**What you'll learn in this guide:** +- What map(), filter(), and reduce() do and when to use each +- The factory assembly line mental model for array transformations +- How to chain methods together for powerful data pipelines +- The critical mistake with reduce() that crashes your code +- Real-world patterns for extracting, filtering, and aggregating data +- Other useful array methods like find(), some(), and every() +- How to implement map and filter using reduce (advanced) +- How to handle async callbacks with these methods +</Info> + +<Warning> +**Prerequisite:** This guide assumes you understand [higher-order functions](/concepts/higher-order-functions). map, filter, and reduce are all higher-order functions that take callbacks. If that concept is new to you, read that guide first! +</Warning> + +--- + +## The Factory Assembly Line + +Think of these three methods as stations on a factory assembly line. Raw materials (your input array) flow through different stations, each performing a specific job: + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ THE FACTORY ASSEMBLY LINE │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Raw Materials PAINTING QUALITY PACKAGING │ +│ (Input Array) STATION CONTROL STATION │ +│ map() filter() reduce() │ +│ │ +│ ┌───┬───┬───┬───┐ ┌───┬───┬───┬───┐ ┌───┬───┐ ┌─────────┐ │ +│ │ 1 │ 2 │ 3 │ 4 │ → │ 2 │ 4 │ 6 │ 8 │ → │ 6 │ 8 │ → │ 14 │ │ +│ └───┴───┴───┴───┘ └───┴───┴───┴───┘ └───┴───┘ └─────────┘ │ +│ │ +│ Transform Keep items Combine into │ +│ each item where n > 4 single value │ +│ (n × 2) (sum) │ +│ │ +│ ──────────────────────────────────────────────────────────────────── │ +│ │ +│ INPUT COUNT SAME COUNT FEWER OR SINGLE │ +│ = 4 items = 4 items SAME = 2 OUTPUT │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +**Painting Station (map):** Every item gets transformed. You put in 4 items, you get 4 items out. Each one is changed in the same way. + +**Quality Control (filter):** Items are inspected and only those that pass the test continue. You might get fewer items out than you put in. + +**Packaging Station (reduce):** Everything gets combined into a single package. Many items go in, one result comes out. + +The beauty of this assembly line? You can connect the stations in any order. The output of one becomes the input of the next. + +--- + +## What Are These Methods? + +These three methods are the workhorses of functional programming in JavaScript. They let you transform, filter, and aggregate data without writing explicit loops and without mutating your original data. + +| Method | What It Does | Returns | Original Array | +|--------|-------------|---------|----------------| +| `map()` | Transforms every element | New array (same length) | Unchanged | +| `filter()` | Keeps elements that pass a test | New array (0 to same length) | Unchanged | +| `reduce()` | Combines all elements into one value | Any type (number, object, array, etc.) | Unchanged | + +<Tip> +**The Immutability Principle:** None of these methods change the original array. They always return something new. This makes your code predictable and easier to debug. +</Tip> + +--- + +## map() — Transform Every Element + +The **[map()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map)** method creates a new array by calling a function on every element of the original array. Think of it as a transformation machine: every item goes in, every item comes out changed. + +### What is map() in JavaScript? + +The `map()` method is an array method that creates a new array by applying a callback function to each element of the original array. It returns an array of the same length with each element transformed according to the callback. The original array is never modified, making map a pure, non-mutating operation ideal for functional programming. + +```javascript +const numbers = [1, 2, 3, 4] +const doubled = numbers.map(num => num * 2) + +console.log(doubled) // [2, 4, 6, 8] +console.log(numbers) // [1, 2, 3, 4] — original unchanged! +``` + +### Syntax and Parameters + +```javascript +array.map(callback(element, index, array), thisArg) +``` + +| Parameter | Description | +|-----------|-------------| +| `element` | The current element being processed | +| `index` | The index of the current element (optional) | +| `array` | The array map() was called on (optional) | +| `thisArg` | Value to use as `this` in callback (optional, rarely used) | + +### Basic Transformations + +```javascript +// Double every number +const numbers = [1, 2, 3, 4, 5] +const doubled = numbers.map(n => n * 2) +console.log(doubled) // [2, 4, 6, 8, 10] + +// Convert to uppercase +const words = ['hello', 'world'] +const shouting = words.map(word => word.toUpperCase()) +console.log(shouting) // ['HELLO', 'WORLD'] + +// Square each number +const squares = numbers.map(n => n * n) +console.log(squares) // [1, 4, 9, 16, 25] +``` + +### Extracting Properties from Objects + +One of the most common uses of map is pulling out specific properties from an array of objects: + +```javascript +const users = [ + { id: 1, name: 'Alice', email: 'alice@example.com' }, + { id: 2, name: 'Bob', email: 'bob@example.com' }, + { id: 3, name: 'Charlie', email: 'charlie@example.com' } +] + +// Get just the names +const names = users.map(user => user.name) +console.log(names) // ['Alice', 'Bob', 'Charlie'] + +// Get just the emails +const emails = users.map(user => user.email) +console.log(emails) // ['alice@example.com', 'bob@example.com', 'charlie@example.com'] + +// Get IDs as strings +const ids = users.map(user => `user-${user.id}`) +console.log(ids) // ['user-1', 'user-2', 'user-3'] +``` + +### Transforming Object Shapes + +You can also reshape objects completely: + +```javascript +const users = [ + { firstName: 'Alice', lastName: 'Smith', age: 25 }, + { firstName: 'Bob', lastName: 'Jones', age: 30 } +] + +const displayUsers = users.map(user => ({ + fullName: `${user.firstName} ${user.lastName}`, + isAdult: user.age >= 18 +})) + +console.log(displayUsers) +// [ +// { fullName: 'Alice Smith', isAdult: true }, +// { fullName: 'Bob Jones', isAdult: true } +// ] +``` + +### Using the Index Parameter + +Sometimes you need to know the position of each element: + +```javascript +const letters = ['a', 'b', 'c', 'd'] + +// Add index to each item +const indexed = letters.map((letter, index) => `${index}: ${letter}`) +console.log(indexed) // ['0: a', '1: b', '2: c', '3: d'] + +// Create objects with IDs +const items = ['apple', 'banana', 'cherry'] +const products = items.map((name, index) => ({ + id: index + 1, + name +})) +console.log(products) +// [{ id: 1, name: 'apple' }, { id: 2, name: 'banana' }, { id: 3, name: 'cherry' }] +``` + +### The parseInt Pitfall + +This is a classic JavaScript gotcha. Can you spot the problem? + +```javascript +const strings = ['1', '2', '3'] +const numbers = strings.map(parseInt) + +console.log(numbers) // [1, NaN, NaN] — Wait, what?! +``` + +**Why does this happen?** Because `parseInt` takes two arguments: the string to parse and the radix (base). When you pass `parseInt` directly to map, it receives three arguments: `(element, index, array)`. So the index becomes the radix! + +```javascript +// What's actually happening: +parseInt('1', 0) // 1 (radix 0 defaults to 10) +parseInt('2', 1) // NaN (radix 1 is invalid) +parseInt('3', 2) // NaN (3 is not a valid digit in binary) +``` + +**The fix:** Wrap parseInt in an arrow function or use `Number`: + +```javascript +// Option 1: Wrap in arrow function +const numbers1 = strings.map(str => parseInt(str, 10)) +console.log(numbers1) // [1, 2, 3] + +// Option 2: Use Number (simpler) +const numbers2 = strings.map(Number) +console.log(numbers2) // [1, 2, 3] +``` + +### map() vs forEach() + +Both iterate over arrays, but they're for different purposes: + +| Aspect | map() | forEach() | +|--------|-------|-----------| +| **Returns** | New array | undefined | +| **Purpose** | Transform data | Side effects (logging, etc.) | +| **Chainable** | Yes | No | +| **Use when** | You need the results | You just want to do something | + +```javascript +const numbers = [1, 2, 3] + +// map: When you need a new array +const doubled = numbers.map(n => n * 2) +console.log(doubled) // [2, 4, 6] + +// forEach: When you just want to do something with each item +numbers.forEach(n => console.log(n)) // Logs 1, 2, 3 + +// ❌ WRONG: Using map for side effects (wasteful) +numbers.map(n => console.log(n)) // Creates unused array [undefined, undefined, undefined] + +// ✓ CORRECT: Use forEach for side effects +numbers.forEach(n => console.log(n)) +``` + +<Warning> +**Don't use map() when you don't need the returned array.** If you're just logging or making API calls, use `forEach()`. Using map for side effects creates an unused array and signals the wrong intent to other developers. +</Warning> + +--- + +## filter() — Keep Matching Elements + +The **[filter()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter)** method creates a new array with only the elements that pass a test. Your callback function returns `true` to keep an element or `false` to exclude it. + +### What is filter() in JavaScript? + +The `filter()` method is an array method that creates a new array containing only the elements that pass a test implemented by a callback function. Elements where the callback returns `true` (or a truthy value) are included; elements where it returns `false` are excluded. Like map, filter never modifies the original array. + +```javascript +const numbers = [1, 2, 3, 4, 5, 6] +const evens = numbers.filter(num => num % 2 === 0) + +console.log(evens) // [2, 4, 6] +console.log(numbers) // [1, 2, 3, 4, 5, 6] — original unchanged! +``` + +### Syntax and Parameters + +```javascript +array.filter(callback(element, index, array), thisArg) +``` + +The callback receives the same parameters as `map()`: `element`, `index`, `array`, plus an optional `thisArg`. + +### Basic Filtering + +```javascript +const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + +// Keep only even numbers +const evens = numbers.filter(n => n % 2 === 0) +console.log(evens) // [2, 4, 6, 8, 10] + +// Keep only odds +const odds = numbers.filter(n => n % 2 !== 0) +console.log(odds) // [1, 3, 5, 7, 9] + +// Keep numbers greater than 5 +const big = numbers.filter(n => n > 5) +console.log(big) // [6, 7, 8, 9, 10] + +// Keep numbers between 3 and 7 +const middle = numbers.filter(n => n >= 3 && n <= 7) +console.log(middle) // [3, 4, 5, 6, 7] +``` + +### Filtering Objects by Property + +```javascript +const users = [ + { name: 'Alice', age: 25, active: true }, + { name: 'Bob', age: 17, active: true }, + { name: 'Charlie', age: 30, active: false }, + { name: 'Diana', age: 22, active: true } +] + +// Keep only active users +const activeUsers = users.filter(user => user.active) +console.log(activeUsers) +// [{ name: 'Alice', ... }, { name: 'Bob', ... }, { name: 'Diana', ... }] + +// Keep only adults (18+) +const adults = users.filter(user => user.age >= 18) +console.log(adults) +// [{ name: 'Alice', ... }, { name: 'Charlie', ... }, { name: 'Diana', ... }] + +// Keep only active adults +const activeAdults = users.filter(user => user.active && user.age >= 18) +console.log(activeAdults) +// [{ name: 'Alice', ... }, { name: 'Diana', ... }] +``` + +### Truthy/Falsy Evaluation + +The filter callback's return value is evaluated for [truthiness](https://developer.mozilla.org/en-US/docs/Glossary/Truthy). This means you can use filter to remove falsy values: + +```javascript +const mixed = [0, 1, '', 'hello', null, undefined, false, true, NaN, 42] + +// Remove all falsy values +const truthy = mixed.filter(Boolean) +console.log(truthy) // [1, 'hello', true, 42] + +// This works because Boolean(value) returns true for truthy values +// Boolean(0) → false +// Boolean(1) → true +// Boolean('') → false +// Boolean('hello') → true +// etc. +``` + +<Note> +**Falsy values in JavaScript:** `false`, `0`, `-0`, `0n` (BigInt), `""` (empty string), `null`, `undefined`, `NaN`. Everything else is truthy. +</Note> + +### Search and Query Filtering + +```javascript +const products = [ + { name: 'MacBook Pro', category: 'laptops', price: 2000 }, + { name: 'iPhone', category: 'phones', price: 1000 }, + { name: 'iPad', category: 'tablets', price: 800 }, + { name: 'Dell XPS', category: 'laptops', price: 1500 } +] + +// Search by name (case-insensitive) +const searchTerm = 'mac' +const results = products.filter(p => + p.name.toLowerCase().includes(searchTerm.toLowerCase()) +) +console.log(results) // [{ name: 'MacBook Pro', ... }] + +// Filter by category +const laptops = products.filter(p => p.category === 'laptops') +console.log(laptops) // [{ name: 'MacBook Pro', ... }, { name: 'Dell XPS', ... }] + +// Filter by price range +const affordable = products.filter(p => p.price <= 1000) +console.log(affordable) // [{ name: 'iPhone', ... }, { name: 'iPad', ... }] +``` + +### filter() vs find() vs some() vs every() + +These methods are related but return different things: + +| Method | Returns | Stops Early? | Use Case | +|--------|---------|--------------|----------| +| `filter()` | Array of all matches | No | Get all matching items | +| `find()` | First match (or undefined) | Yes | Get one item by condition | +| `some()` | true/false | Yes | Check if any match | +| `every()` | true/false | Yes | Check if all match | + +```javascript +const numbers = [1, 2, 3, 4, 5] + +// filter: Get ALL even numbers +numbers.filter(n => n % 2 === 0) // [2, 4] + +// find: Get the FIRST even number +numbers.find(n => n % 2 === 0) // 2 + +// some: Is there ANY even number? +numbers.some(n => n % 2 === 0) // true + +// every: Are ALL numbers even? +numbers.every(n => n % 2 === 0) // false +``` + +```javascript +const users = [ + { id: 1, name: 'Alice', admin: true }, + { id: 2, name: 'Bob', admin: false }, + { id: 3, name: 'Charlie', admin: false } +] + +// ❌ INEFFICIENT: Using filter when you only need one +const result = users.filter(u => u.id === 2)[0] // Checks all elements + +// ✓ EFFICIENT: Use find for single item lookup +const user = users.find(u => u.id === 2) // Stops at first match + +// ❌ WASTEFUL: Using filter just to check existence +const hasAdmin = users.filter(u => u.admin).length > 0 + +// ✓ BETTER: Use some for existence check +const hasAdmin2 = users.some(u => u.admin) // true, stops at first admin +``` + +--- + +## reduce() — Combine Into One Value + +The **[reduce()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce)** method executes a "reducer" function on each element, resulting in a single output value. It's the most powerful (and most confusing) of the three. + +### What is reduce() in JavaScript? + +The `reduce()` method is an array method that executes a reducer callback function on each element, accumulating the results into a single value. This value can be any type: a number, string, object, or even another array. The callback receives an accumulator (the running total) and the current element, returning the new accumulator value. Always provide an initial value to avoid crashes on empty arrays. + +```javascript +const numbers = [1, 2, 3, 4, 5] +const sum = numbers.reduce((accumulator, current) => accumulator + current, 0) + +console.log(sum) // 15 +``` + +Think of reduce like a snowball rolling down a hill. It starts small (the initial value) and grows as it picks up each element. + +### The Anatomy of reduce() + +```javascript +array.reduce(callback(accumulator, currentValue, index, array), initialValue) +``` + +| Parameter | Description | +|-----------|-------------| +| `accumulator` | The accumulated value from previous iterations | +| `currentValue` | The current element being processed | +| `index` | The index of the current element (optional) | +| `array` | The array reduce() was called on (optional) | +| `initialValue` | The starting value for the accumulator (ALWAYS provide this!) | + +### Step-by-Step Visualization + +Let's trace through how reduce works: + +```javascript +const numbers = [1, 2, 3, 4] +const sum = numbers.reduce((acc, curr) => acc + curr, 0) +``` + +| Iteration | accumulator | currentValue | acc + curr | New accumulator | +|-----------|-------------|--------------|------------|-----------------| +| 1st | 0 (initial) | 1 | 0 + 1 | 1 | +| 2nd | 1 | 2 | 1 + 2 | 3 | +| 3rd | 3 | 3 | 3 + 3 | 6 | +| 4th | 6 | 4 | 6 + 4 | **10** | + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ reduce() STEP BY STEP │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Initial value: 0 │ +│ │ +│ [1, 2, 3, 4].reduce((acc, curr) => acc + curr, 0) │ +│ │ +│ Step 1: acc=0, curr=1 → 0 + 1 = 1 (accumulator becomes 1) │ +│ Step 2: acc=1, curr=2 → 1 + 2 = 3 (accumulator becomes 3) │ +│ Step 3: acc=3, curr=3 → 3 + 3 = 6 (accumulator becomes 6) │ +│ Step 4: acc=6, curr=4 → 6 + 4 = 10 (final result!) │ +│ │ +│ Result: 10 │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### Common Use Cases + +#### Sum and Average + +```javascript +const numbers = [10, 20, 30, 40, 50] + +// Sum +const sum = numbers.reduce((acc, n) => acc + n, 0) +console.log(sum) // 150 + +// Average +const average = numbers.reduce((acc, n) => acc + n, 0) / numbers.length +console.log(average) // 30 +``` + +#### Finding Max/Min + +```javascript +const numbers = [5, 2, 9, 1, 7] + +const max = numbers.reduce((acc, n) => n > acc ? n : acc, numbers[0]) +console.log(max) // 9 + +const min = numbers.reduce((acc, n) => n < acc ? n : acc, numbers[0]) +console.log(min) // 1 + +// Or use Math.max/min with spread (simpler for this case) +console.log(Math.max(...numbers)) // 9 +console.log(Math.min(...numbers)) // 1 +``` + +#### Counting Occurrences + +```javascript +const fruits = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple'] + +const count = fruits.reduce((acc, fruit) => { + acc[fruit] = (acc[fruit] || 0) + 1 + return acc +}, {}) + +console.log(count) // { apple: 3, banana: 2, orange: 1 } +``` + +#### Grouping by Property + +```javascript +const people = [ + { name: 'Alice', department: 'Engineering' }, + { name: 'Bob', department: 'Marketing' }, + { name: 'Charlie', department: 'Engineering' }, + { name: 'Diana', department: 'Marketing' } +] + +const byDepartment = people.reduce((acc, person) => { + const dept = person.department + if (!acc[dept]) { + acc[dept] = [] + } + acc[dept].push(person) + return acc +}, {}) + +console.log(byDepartment) +// { +// Engineering: [{ name: 'Alice', ... }, { name: 'Charlie', ... }], +// Marketing: [{ name: 'Bob', ... }, { name: 'Diana', ... }] +// } +``` + +<Note> +**Modern Alternative (ES2024):** JavaScript now has `Object.groupBy()` which does this in one line: + +```javascript +const byDepartment = Object.groupBy(people, person => person.department) +``` + +This is cleaner for simple grouping, but reduce is still useful when you need custom accumulation logic. Note: `Object.groupBy()` requires Node.js 21+ or modern browsers (Chrome 117+, Firefox 119+, Safari 17.4+). +</Note> + +#### Building Objects from Arrays + +```javascript +const pairs = [['name', 'Alice'], ['age', 25], ['city', 'NYC']] + +const obj = pairs.reduce((acc, [key, value]) => { + acc[key] = value + return acc +}, {}) + +console.log(obj) // { name: 'Alice', age: 25, city: 'NYC' } +``` + +#### Flattening Nested Arrays + +```javascript +const nested = [[1, 2], [3, 4], [5, 6]] + +const flat = nested.reduce((acc, arr) => acc.concat(arr), []) +console.log(flat) // [1, 2, 3, 4, 5, 6] + +// Note: For simple flattening, use .flat() instead +console.log(nested.flat()) // [1, 2, 3, 4, 5, 6] +``` + +### Implementing map() and filter() with reduce() + +This shows just how powerful reduce is. You can implement both map and filter using reduce: + +```javascript +// map() implemented with reduce +function myMap(array, callback) { + return array.reduce((acc, element, index) => { + acc.push(callback(element, index, array)) + return acc + }, []) +} + +const doubled = myMap([1, 2, 3], n => n * 2) +console.log(doubled) // [2, 4, 6] + + +// filter() implemented with reduce +function myFilter(array, callback) { + return array.reduce((acc, element, index) => { + if (callback(element, index, array)) { + acc.push(element) + } + return acc + }, []) +} + +const evens = myFilter([1, 2, 3, 4, 5], n => n % 2 === 0) +console.log(evens) // [2, 4] +``` + +<Tip> +**When to use reduce:** Use reduce when you need to transform an array into a different type (array to object, array to number, etc.) or when you need complex accumulation logic. For simple transformations, map and filter are usually clearer. +</Tip> + +--- + +## Method Chaining — The Real Power + +The real magic happens when you chain these methods together. Each method returns a new array (or value), which you can immediately call another method on. + +```javascript +const transactions = [ + { type: 'sale', amount: 100 }, + { type: 'refund', amount: 30 }, + { type: 'sale', amount: 200 }, + { type: 'sale', amount: 150 }, + { type: 'refund', amount: 50 } +] + +const totalSales = transactions + .filter(t => t.type === 'sale') // Keep only sales + .map(t => t.amount) // Extract amounts + .reduce((sum, amount) => sum + amount, 0) // Sum them up + +console.log(totalSales) // 450 +``` + +### Reading Chained Methods + +When you see a chain, read it like a data pipeline. Data flows from top to bottom, transformed at each step: + +```javascript +const result = data + .filter(...) // Step 1: Remove unwanted items + .map(...) // Step 2: Transform remaining items + .filter(...) // Step 3: Filter again if needed + .reduce(...) // Step 4: Combine into final result +``` + +### Real-World Examples + +#### E-commerce: Calculate discounted total + +```javascript +const cart = [ + { name: 'Laptop', price: 1000, quantity: 1, discountPercent: 10 }, + { name: 'Mouse', price: 50, quantity: 2, discountPercent: 0 }, + { name: 'Keyboard', price: 100, quantity: 1, discountPercent: 20 } +] + +const total = cart + .map(item => { + const subtotal = item.price * item.quantity + const discount = subtotal * (item.discountPercent / 100) + return subtotal - discount + }) + .reduce((sum, price) => sum + price, 0) + +console.log(total) // 900 + 100 + 80 = 1080 +``` + +#### User dashboard: Get active premium users' emails + +```javascript +const users = [ + { email: 'alice@example.com', active: true, plan: 'premium' }, + { email: 'bob@example.com', active: false, plan: 'premium' }, + { email: 'charlie@example.com', active: true, plan: 'free' }, + { email: 'diana@example.com', active: true, plan: 'premium' } +] + +const premiumEmails = users + .filter(u => u.active) + .filter(u => u.plan === 'premium') + .map(u => u.email) + +console.log(premiumEmails) // ['alice@example.com', 'diana@example.com'] +``` + +#### Analytics: Top 3 performers + +```javascript +const salespeople = [ + { name: 'Alice', sales: 50000 }, + { name: 'Bob', sales: 75000 }, + { name: 'Charlie', sales: 45000 }, + { name: 'Diana', sales: 90000 }, + { name: 'Eve', sales: 60000 } +] + +const top3 = salespeople + .filter(p => p.sales >= 50000) // Minimum threshold + .sort((a, b) => b.sales - a.sales) // Sort descending + .slice(0, 3) // Take top 3 + .map(p => p.name) // Get just names + +console.log(top3) // ['Diana', 'Bob', 'Eve'] +``` + +### Performance Considerations + +Each method in a chain iterates over the array. For small arrays, this doesn't matter. For large arrays, consider combining operations: + +```javascript +const hugeArray = Array.from({ length: 100000 }, (_, i) => i) + +// ❌ SLOW: Three separate iterations +const result1 = hugeArray + .filter(n => n % 2 === 0) // Iteration 1 + .map(n => n * 2) // Iteration 2 + .filter(n => n > 1000) // Iteration 3 + +// ✓ FASTER: Single iteration with reduce +const result2 = hugeArray.reduce((acc, n) => { + if (n % 2 === 0) { + const doubled = n * 2 + if (doubled > 1000) { + acc.push(doubled) + } + } + return acc +}, []) +``` + +<Tip> +**Performance Rule of Thumb:** For arrays under 10,000 items, prioritize readability. For larger arrays or performance-critical code, consider combining operations into a single reduce. +</Tip> + +--- + +## The #1 Mistake: Forgetting reduce()'s Initial Value + +This is the most common mistake developers make with reduce, and it can crash your application: + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ THE INITIAL VALUE PROBLEM │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ❌ WITHOUT INITIAL VALUE ✓ WITH INITIAL VALUE │ +│ ───────────────────────── ──────────────────── │ +│ │ +│ [1, 2, 3].reduce((a,b) => a+b) [1, 2, 3].reduce((a,b) => a+b, 0) │ +│ → Works: 6 → Works: 6 │ +│ │ +│ [].reduce((a,b) => a+b) [].reduce((a,b) => a+b, 0) │ +│ → TypeError! CRASH → Works: 0 │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### Empty Array Without Initial Value = CRASH + +```javascript +// ❌ DANGEROUS: No initial value +const numbers = [] +const sum = numbers.reduce((acc, n) => acc + n) +// TypeError: Reduce of empty array with no initial value + +// ✓ SAFE: Always provide initial value +const safeSum = numbers.reduce((acc, n) => acc + n, 0) +console.log(safeSum) // 0 +``` + +### Type Mismatch Without Initial Value + +```javascript +const products = [ + { name: 'Laptop', price: 1000 }, + { name: 'Phone', price: 500 } +] + +// ❌ WRONG: First accumulator will be the first object, not a number! +const total = products.reduce((acc, p) => acc + p.price) +console.log(total) // "[object Object]500" — Oops! + +// ✓ CORRECT: Initial value sets the accumulator type +const total2 = products.reduce((acc, p) => acc + p.price, 0) +console.log(total2) // 1500 +``` + +<Warning> +**The Rule:** Always provide an initial value to reduce(). It prevents crashes on empty arrays and makes your code's intent clear. +</Warning> + +--- + +## Common Mistakes + +### map() Mistakes + +#### Forgetting to Return + +```javascript +const numbers = [1, 2, 3] + +// ❌ WRONG: No return statement +const doubled = numbers.map(n => { + n * 2 // Missing return! +}) +console.log(doubled) // [undefined, undefined, undefined] + +// ✓ CORRECT: Explicit return +const doubled2 = numbers.map(n => { + return n * 2 +}) +console.log(doubled2) // [2, 4, 6] + +// ✓ CORRECT: Implicit return (no curly braces) +const doubled3 = numbers.map(n => n * 2) +console.log(doubled3) // [2, 4, 6] +``` + +#### Mutating Original Objects + +```javascript +const users = [ + { name: 'Alice', score: 85 }, + { name: 'Bob', score: 92 } +] + +// ❌ WRONG: Mutates the original objects +const curved = users.map(user => { + user.score += 5 // Mutates original! + return user +}) + +console.log(users[0].score) // 90 — Original was changed! + +// ✓ CORRECT: Create new objects +const users2 = [ + { name: 'Alice', score: 85 }, + { name: 'Bob', score: 92 } +] + +const curved2 = users2.map(user => ({ + ...user, + score: user.score + 5 +})) + +console.log(users2[0].score) // 85 — Original unchanged +console.log(curved2[0].score) // 90 +``` + +### filter() Mistakes + +#### Using filter When find is Better + +```javascript +const users = [ + { id: 1, name: 'Alice' }, + { id: 2, name: 'Bob' }, + { id: 3, name: 'Charlie' } +] + +// ❌ INEFFICIENT: Checks entire array, returns array, needs [0] +const user = users.filter(u => u.id === 2)[0] + +// ✓ EFFICIENT: Stops at first match, returns the item directly +const user2 = users.find(u => u.id === 2) +``` + +### reduce() Mistakes + +#### Not Returning the Accumulator + +```javascript +const numbers = [1, 2, 3, 4] + +// ❌ WRONG: Forgetting to return accumulator +const sum = numbers.reduce((acc, n) => { + acc + n // Missing return! +}, 0) +console.log(sum) // undefined + +// ✓ CORRECT: Always return the accumulator +const sum2 = numbers.reduce((acc, n) => { + return acc + n +}, 0) +console.log(sum2) // 10 +``` + +#### Making reduce Too Complex + +```javascript +const users = [ + { name: 'Alice', active: true }, + { name: 'Bob', active: false }, + { name: 'Charlie', active: true } +] + +// ❌ HARD TO READ: Everything crammed into reduce +const result = users.reduce((acc, user) => { + if (user.active) { + acc.push(user.name.toUpperCase()) + } + return acc +}, []) + +// ✓ CLEARER: Use filter + map +const result2 = users + .filter(u => u.active) + .map(u => u.name.toUpperCase()) + +console.log(result2) // ['ALICE', 'CHARLIE'] +``` + +--- + +## Other Useful Array Methods + +JavaScript has many more array methods beyond map, filter, and reduce. Here's a quick reference: + +| Method | Returns | Description | +|--------|---------|-------------| +| `find(fn)` | Element or undefined | First element that passes test | +| `findIndex(fn)` | Number | Index of first match (-1 if none) | +| `some(fn)` | Boolean | True if any element passes test | +| `every(fn)` | Boolean | True if all elements pass test | +| `includes(value)` | Boolean | True if value is in array | +| `indexOf(value)` | Number | Index of value (-1 if not found) | +| `flat(depth)` | Array | Flattens nested arrays | +| `flatMap(fn)` | Array | map() then flat(1) | +| `forEach(fn)` | undefined | Executes function for side effects | +| `reduceRight(fn, init)` | Any | Like reduce(), but right-to-left | +| `sort(fn)` | Array | Sorts in place (mutates!) | +| `toSorted(fn)` | Array | Returns sorted copy (no mutation) — ES2023 | +| `reverse()` | Array | Reverses in place (mutates!) | +| `toReversed()` | Array | Returns reversed copy (no mutation) — ES2023 | +| `slice(start, end)` | Array | Returns portion (no mutation) | +| `splice(start, count)` | Array | Removes/adds elements (mutates!) | +| `toSpliced(start, count)` | Array | Returns modified copy (no mutation) — ES2023 | + +### Quick Examples + +```javascript +const numbers = [1, 2, 3, 4, 5] + +// find: Get first even number +numbers.find(n => n % 2 === 0) // 2 + +// findIndex: Get index of first even +numbers.findIndex(n => n % 2 === 0) // 1 + +// some: Is there any number > 4? +numbers.some(n => n > 4) // true + +// every: Are all numbers positive? +numbers.every(n => n > 0) // true + +// includes: Is 3 in the array? +numbers.includes(3) // true + +// flat: Flatten nested arrays +[[1, 2], [3, 4]].flat() // [1, 2, 3, 4] + +// flatMap: Map and flatten +[1, 2].flatMap(n => [n, n * 2]) // [1, 2, 2, 4] + +// reduceRight: Reduce from right to left +['a', 'b', 'c'].reduceRight((acc, s) => acc + s, '') // 'cba' + +// toSorted: Non-mutating sort (ES2023) +const nums = [3, 1, 2] +const sorted = nums.toSorted() // [1, 2, 3] +console.log(nums) // [3, 1, 2] — original unchanged! + +// toReversed: Non-mutating reverse (ES2023) +const reversed = nums.toReversed() // [2, 1, 3] +``` + +### Which Method Should I Use? + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ CHOOSING THE RIGHT METHOD │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ WHAT DO YOU NEED? USE THIS │ +│ ───────────────── ──────── │ +│ │ +│ Transform every element → map() │ +│ Keep some elements → filter() │ +│ Combine into single value → reduce() │ +│ Find first matching element → find() │ +│ Check if any element matches → some() │ +│ Check if all elements match → every() │ +│ Check if value exists → includes() │ +│ Get index of element → findIndex() or indexOf() │ +│ Just do something with each → forEach() │ +│ Flatten nested arrays → flat() or flatMap() │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Async Callbacks: The Hidden Gotcha + +One thing that catches developers off guard: `map()`, `filter()`, and `reduce()` don't wait for async callbacks. They run synchronously, which means you'll get an array of Promises instead of resolved values. + +```javascript +const userIds = [1, 2, 3] + +// ❌ WRONG: Returns array of Promises, not users +const users = userIds.map(async (id) => { + const response = await fetch(`/api/users/${id}`) + return response.json() +}) + +console.log(users) // [Promise, Promise, Promise] +``` + +### The Solution: Promise.all() + +Wrap the map result in `Promise.all()` to wait for all Promises to resolve: + +```javascript +const userIds = [1, 2, 3] + +// ✓ CORRECT: Wait for all Promises to resolve +const users = await Promise.all( + userIds.map(async (id) => { + const response = await fetch(`/api/users/${id}`) + return response.json() + }) +) + +console.log(users) // [{...}, {...}, {...}] — actual user objects +``` + +### Async Filter is Trickier + +For filter, you need a two-step approach since filter expects a boolean, not a Promise: + +```javascript +const numbers = [1, 2, 3, 4, 5] + +// Check if a number is "valid" via async operation +async function isValid(n) { + // Imagine this calls an API + return n % 2 === 0 +} + +// ❌ WRONG: filter doesn't await +const evens = numbers.filter(async (n) => await isValid(n)) +console.log(evens) // [1, 2, 3, 4, 5] — all items! (Promises are truthy) + +// ✓ CORRECT: Map to booleans first, then filter +const checks = await Promise.all(numbers.map(n => isValid(n))) +const evens2 = numbers.filter((_, index) => checks[index]) +console.log(evens2) // [2, 4] +``` + +<Tip> +**For sequential async operations** (when order matters or you need to limit concurrency), use a `for...of` loop instead of map. Array methods run all callbacks immediately in parallel. +</Tip> + +--- + +## Key Takeaways + +<Info> +**The key things to remember:** + +1. **map() transforms every element** — Input array length equals output array length. Use it to change each item in the same way. + +2. **filter() keeps matching elements** — Returns 0 to all elements. Use it to remove items that don't pass a test. + +3. **reduce() combines into one value** — Can return any type: number, string, object, array. The "Swiss Army knife" of array methods. + +4. **None of these mutate the original array** — They always return something new. This makes your code predictable. + +5. **Always provide reduce()'s initial value** — Empty arrays without an initial value crash. Don't risk it. + +6. **Chain methods for powerful pipelines** — filter → map → reduce is a common pattern for data processing. + +7. **map() must return something** — Forgetting the return statement gives you an array of undefined. + +8. **Don't use map() for side effects** — Use forEach() if you just want to do something with each element. + +9. **Use find() for single item lookup** — It's more efficient than filter()[0] because it stops at the first match. + +10. **Async callbacks need Promise.all()** — map/filter/reduce don't wait for async callbacks. Wrap in Promise.all() to resolve them. +</Info> + +--- + +## Test Your Knowledge + +<AccordionGroup> + <Accordion title="Question 1: What does map() return if the callback doesn't return anything?"> + **Answer:** + + An array of `undefined` values. In JavaScript, functions without an explicit return statement return `undefined`. + + ```javascript + const numbers = [1, 2, 3] + + // Missing return + const result = numbers.map(n => { + n * 2 // No return! + }) + + console.log(result) // [undefined, undefined, undefined] + ``` + + Always remember to return a value from your map callback, or use implicit return (arrow function without curly braces). + </Accordion> + + <Accordion title="Question 2: What's the difference between filter() and find()?"> + **Answer:** + + - **filter()** returns an **array** of all matching elements (could be empty) + - **find()** returns the **first matching element** (or undefined if none) + + ```javascript + const numbers = [1, 2, 3, 4, 5, 6] + + numbers.filter(n => n % 2 === 0) // [2, 4, 6] — All matches + numbers.find(n => n % 2 === 0) // 2 — First match only + ``` + + Use `find()` when you only need one result. It's more efficient because it stops searching after the first match. + </Accordion> + + <Accordion title="Question 3: Why does reduce() crash on empty arrays without an initial value?"> + **Answer:** + + Without an initial value, reduce uses the first element as the starting accumulator. If the array is empty, there's no first element, so JavaScript throws a TypeError. + + ```javascript + // No initial value + empty array = crash + [].reduce((acc, n) => acc + n) + // TypeError: Reduce of empty array with no initial value + + // With initial value, empty array returns the initial value + [].reduce((acc, n) => acc + n, 0) // 0 + ``` + + Always provide an initial value to prevent crashes and make your intent clear. + </Accordion> + + <Accordion title="Question 4: What's wrong with this code?"> + ```javascript + const doubled = numbers.map(n => { n * 2 }) + ``` + + **Answer:** + + The curly braces `{}` create a function body, which requires an explicit `return` statement. Without it, the function returns `undefined`. + + ```javascript + // ❌ Wrong (returns undefined) + const doubled = numbers.map(n => { n * 2 }) + + // ✓ Correct (explicit return) + const doubled = numbers.map(n => { return n * 2 }) + + // ✓ Correct (implicit return, no braces) + const doubled = numbers.map(n => n * 2) + ``` + </Accordion> + + <Accordion title="Question 5: How would you get the total price of in-stock items?"> + ```javascript + const products = [ + { name: 'Laptop', price: 1000, inStock: true }, + { name: 'Phone', price: 500, inStock: false }, + { name: 'Tablet', price: 300, inStock: true } + ] + ``` + + **Answer:** + + Chain filter → map → reduce: + + ```javascript + const total = products + .filter(p => p.inStock) // Keep only in-stock + .map(p => p.price) // Extract prices + .reduce((sum, p) => sum + p, 0) // Sum them + + console.log(total) // 1300 + + // Or combine map and reduce: + const total2 = products + .filter(p => p.inStock) + .reduce((sum, p) => sum + p.price, 0) + + console.log(total2) // 1300 + ``` + </Accordion> + + <Accordion title="Question 6: What's the output of this code?"> + ```javascript + const result = [1, 2, 3, 4, 5] + .filter(n => n % 2 === 0) + .map(n => n * 3) + .reduce((sum, n) => sum + n, 0) + + console.log(result) + ``` + + **Answer:** + + **18** + + Let's trace through: + 1. `filter(n => n % 2 === 0)` keeps even numbers: `[2, 4]` + 2. `map(n => n * 3)` triples each: `[6, 12]` + 3. `reduce((sum, n) => sum + n, 0)` sums them: `0 + 6 + 12 = 18` + </Accordion> +</AccordionGroup> + +--- + +## Related Concepts + +<CardGroup cols={2}> + <Card title="Higher-Order Functions" icon="layer-group" href="/concepts/higher-order-functions"> + map, filter, and reduce are all higher-order functions that take callbacks + </Card> + <Card title="Pure Functions" icon="flask" href="/concepts/pure-functions"> + Why these methods don't mutate and how immutability makes code predictable + </Card> + <Card title="Callbacks" icon="phone" href="/concepts/callbacks"> + The callback pattern that powers these array methods + </Card> + <Card title="Recursion" icon="rotate" href="/concepts/recursion"> + An alternative approach to processing arrays with function calls + </Card> +</CardGroup> + +--- + +## Reference + +<CardGroup cols={2}> + <Card title="Array.prototype.map() — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map"> + Complete documentation for the map method + </Card> + <Card title="Array.prototype.filter() — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter"> + Complete documentation for the filter method + </Card> + <Card title="Array.prototype.reduce() — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce"> + Complete documentation for the reduce method + </Card> + <Card title="Array — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array"> + Overview of all array methods in JavaScript + </Card> +</CardGroup> + +## Articles + +<CardGroup cols={2}> + <Card title="Array Methods — javascript.info" icon="newspaper" href="https://javascript.info/array-methods"> + Comprehensive reference covering all array methods with interactive examples. Includes a visual diagram for reduce and 13 practice tasks with solutions. + </Card> + <Card title="An Illustrated Guide to Map, Reduce, and Filter" icon="newspaper" href="https://css-tricks.com/an-illustrated-and-musical-guide-to-map-reduce-and-filter-array-methods/"> + Hand-drawn illustrations make these concepts stick. Filter as a strainer, reduce as cooking sauce. Even includes a song to help you remember! + </Card> + <Card title="Map, Reduce, and Filter Explained with Examples" icon="newspaper" href="https://www.freecodecamp.org/news/javascript-map-reduce-and-filter-explained-with-examples/"> + Concise, beginner-friendly introduction with clean code examples. Gets straight to practical usage without overwhelming theory. + </Card> + <Card title="Differences Between forEach and map" icon="newspaper" href="https://www.freecodecamp.org/news/4-main-differences-between-foreach-and-map/"> + Explains the 4 key differences with side-by-side comparisons. Includes performance testing code you can run yourself. + </Card> + <Card title="Simplify Your JavaScript with map, reduce, filter" icon="newspaper" href="https://medium.com/poka-techblog/simplify-your-javascript-use-map-reduce-and-filter-bd02c593cc2d"> + Star Wars themed examples make learning fun. Excellent section on method chaining and building elegant data pipelines. + </Card> + <Card title="How to Write Your Own map, filter, reduce" icon="newspaper" href="https://www.freecodecamp.org/news/how-to-write-your-own-map-filter-and-reduce-functions-in-javascript-ab1e35679d26/"> + Build these methods from scratch to understand how they work internally. Great for interview prep and deepening your knowledge. + </Card> +</CardGroup> + +## Videos + +<CardGroup cols={2}> + <Card title="Higher-order Functions" icon="video" href="https://www.youtube.com/watch?v=BMUiFMZr7vk"> + Fun Fun Function's legendary intro to functional programming. Mattias explains the mental model that makes map, filter, reduce click. + </Card> + <Card title="Reduce Basics" icon="video" href="https://www.youtube.com/watch?v=Wl98eZpkp-c"> + The hardest method gets its own deep-dive. Clear accumulator examples that finally make reduce make sense. + </Card> + <Card title="Higher Order Functions & Arrays" icon="video" href="https://www.youtube.com/watch?v=rRgD1yVwIvE"> + Traversy Media's complete crash course with live coding. Covers forEach, map, filter, reduce, sort, and find in one session. + </Card> + <Card title="8 Must Know JavaScript Array Methods" icon="video" href="https://www.youtube.com/watch?v=R8rmfD9Y5-c"> + Web Dev Simplified covers the most useful methods in 12 focused minutes. Perfect for a quick refresher on when to use what. + </Card> + <Card title="Map, Filter & Reduce — Namaste JavaScript" icon="video" href="https://www.youtube.com/watch?v=zdp0zrpKzIE"> + Akshay Saini's interview-prep deep-dive. Includes polyfill implementations and common interview questions about these methods. + </Card> +</CardGroup> diff --git a/docs/concepts/modern-js-syntax.mdx b/docs/concepts/modern-js-syntax.mdx new file mode 100644 index 00000000..4f77fddd --- /dev/null +++ b/docs/concepts/modern-js-syntax.mdx @@ -0,0 +1,1182 @@ +--- +title: "Modern JavaScript Syntax: ES6+ Features That Changed Everything" +sidebarTitle: "Modern JavaScript Syntax: ES6+ Features" +description: "Learn modern JavaScript ES6+ syntax. Covers destructuring, spread/rest operators, arrow functions, optional chaining, nullish coalescing, and template literals with practical examples." +--- + +Why does JavaScript code written in 2015 look so different from code written today? How do developers write such concise, readable code without all the boilerplate? + +```javascript +// The old way (pre-ES6) +var city = user && user.address && user.address.city; // undefined if missing +var copy = arr.slice(); +var merged = Object.assign({}, obj1, obj2); + +// The modern way +const city = user?.address?.city; // undefined if missing +const copy = [...arr]; +const merged = { ...obj1, ...obj2 }; +``` + +The answer is **ES6 (ECMAScript 2015)** and the yearly updates that followed. These additions didn't just add features. They transformed how we write JavaScript. Features like [destructuring](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment), [arrow functions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions), and [optional chaining](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining) are now everywhere: in tutorials, open-source projects, and job interviews. + +<Info> +**What you'll learn in this guide:** +- Arrow functions and how they handle `this` differently +- Destructuring objects and arrays to extract values cleanly +- Spread operator (`...`) for copying and merging +- Rest parameters for collecting function arguments +- Template literals for string interpolation +- Optional chaining (`?.`) to avoid "cannot read property of undefined" +- Nullish coalescing (`??`) vs logical OR (`||`) +- Logical assignment operators (`??=`, `||=`, `&&=`) +- Default parameters for functions +- Enhanced object literals (shorthand syntax) +- Map, Set, and Symbol basics +- The `for...of` loop for iterating values +</Info> + +<Warning> +**Prerequisite:** This guide touches on `let`, `const`, and `var` briefly. For a deep dive into how they differ (block scope, hoisting, temporal dead zone), read our [Scope and Closures](/concepts/scope-and-closures) guide first. +</Warning> + +--- + +## A Quick Note on let, const, and var + +Before ES6, `var` was the only way to declare variables. Now we have `let` and `const`, which behave differently: + +| Feature | `var` | `let` | `const` | +|---------|-------|-------|---------| +| Scope | Function | Block | Block | +| Hoisting | Yes (undefined) | Yes (TDZ) | Yes (TDZ) | +| Redeclaration | Allowed | Error | Error | +| Reassignment | Allowed | Allowed | Error | + +```javascript +// var is function-scoped (can cause bugs) +for (var i = 0; i < 3; i++) { + setTimeout(() => console.log(i), 100); +} +// Output: 3, 3, 3 + +// let is block-scoped (each iteration gets its own i) +for (let i = 0; i < 3; i++) { + setTimeout(() => console.log(i), 100); +} +// Output: 0, 1, 2 +``` + +**The modern rule:** Use `const` by default. Use `let` when you need to reassign. Avoid `var`. + +For the full explanation of scope, hoisting, and the temporal dead zone, see [Scope and Closures](/concepts/scope-and-closures). + +--- + +## Arrow Functions + +[Arrow functions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions) provide a shorter syntax for writing functions. But the real difference is how they handle `this`. + +### The Syntax + +```javascript +// Traditional function +function add(a, b) { + return a + b; +} + +// Arrow function variations +const add = (a, b) => a + b; // Implicit return (single expression) +const add = (a, b) => { return a + b; }; // Block body (explicit return needed) +const square = x => x * x; // Single param: parentheses optional +const greet = () => 'Hello!'; // No params: parentheses required +``` + +### Arrow Functions and `this` + +Here's the big difference: arrow functions don't have their own `this`. They inherit `this` from the surrounding code (lexical scope). + +```javascript +// Problem with regular functions +const counter = { + count: 0, + start: function() { + setInterval(function() { + this.count++; // 'this' is NOT the counter object! + console.log(this.count); + }, 1000); + } +}; +counter.start(); // NaN, NaN, NaN... + +// Solution with arrow functions +const counter = { + count: 0, + start: function() { + setInterval(() => { + this.count++; // 'this' IS the counter object + console.log(this.count); + }, 1000); + } +}; +counter.start(); // 1, 2, 3... +``` + +For a complete exploration of `this` binding rules, see [this, call, apply and bind](/concepts/this-call-apply-bind). + +### When NOT to Use Arrow Functions + +Arrow functions aren't always the right choice: + +```javascript +// ❌ DON'T use as object methods +const user = { + name: 'Alice', + greet: () => { + console.log(`Hi, I'm ${this.name}`); // 'this' is NOT user! + } +}; +user.greet(); // "Hi, I'm undefined" + +// ✓ USE regular function for methods +const user = { + name: 'Alice', + greet() { + console.log(`Hi, I'm ${this.name}`); + } +}; +user.greet(); // "Hi, I'm Alice" + +// ❌ DON'T use as constructors +const Person = (name) => { this.name = name; }; +new Person('Alice'); // TypeError: Person is not a constructor + +// ❌ Arrow functions don't have their own 'arguments' +const logArgs = () => console.log(arguments); // ReferenceError (use ...rest instead) +``` + +### The Object Literal Trap + +Returning an object literal requires parentheses: + +```javascript +// ❌ WRONG - curly braces are interpreted as function body +const createUser = name => { name: name }; +console.log(createUser('Alice')); // undefined (it's a labeled statement!) + +// ❌ ALSO WRONG - adding more properties causes a SyntaxError +// const createUser = name => { name: name, active: true }; // SyntaxError! + +// ✓ CORRECT - wrap object literal in parentheses +const createUser = name => ({ name: name, active: true }); +console.log(createUser('Alice')); // { name: 'Alice', active: true } +``` + +--- + +## Destructuring Assignment + +[Destructuring](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) lets you unpack values from arrays or properties from objects into distinct variables. + +### Array Destructuring + +```javascript +const colors = ['red', 'green', 'blue']; + +// Basic destructuring +const [first, second, third] = colors; +console.log(first); // "red" +console.log(second); // "green" + +// Skip elements with empty slots +const [primary, , tertiary] = colors; +console.log(tertiary); // "blue" + +// Default values +const [a, b, c, d = 'yellow'] = colors; +console.log(d); // "yellow" + +// Rest pattern (collect remaining elements) +const [head, ...tail] = colors; +console.log(head); // "red" +console.log(tail); // ["green", "blue"] +``` + +**Swap variables without a temp:** + +```javascript +let x = 1; +let y = 2; + +[x, y] = [y, x]; + +console.log(x); // 2 +console.log(y); // 1 +``` + +### Object Destructuring + +```javascript +const user = { + name: 'Alice', + age: 25, + address: { + city: 'Portland', + country: 'USA' + } +}; + +// Basic destructuring +const { name, age } = user; +console.log(name); // "Alice" + +// Rename variables +const { name: userName, age: userAge } = user; +console.log(userName); // "Alice" + +// Default values +const { name, role = 'guest' } = user; +console.log(role); // "guest" + +// Nested destructuring +const { address: { city } } = user; +console.log(city); // "Portland" + +// Rest pattern +const { name, ...rest } = user; +console.log(rest); // { age: 25, address: { city: 'Portland', country: 'USA' } } +``` + +### Destructuring in Function Parameters + +This pattern is everywhere in modern JavaScript: + +```javascript +// Without destructuring +function createUser(options) { + const name = options.name; + const age = options.age || 18; + const role = options.role || 'user'; + return { name, age, role }; +} + +// With destructuring +function createUser({ name, age = 18, role = 'user' }) { + return { name, age, role }; +} + +// With default for the entire parameter (prevents error if called with no args) +function greet({ name = 'Guest' } = {}) { + return `Hello, ${name}!`; +} + +greet(); // "Hello, Guest!" +greet({ name: 'Alice' }); // "Hello, Alice!" +``` + +### Common Mistake: Destructuring to Existing Variables + +```javascript +let name, age; + +// ❌ WRONG - JavaScript thinks {} is a code block +{ name, age } = user; // SyntaxError + +// ✓ CORRECT - wrap in parentheses +({ name, age } = user); +``` + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ DESTRUCTURING VISUALIZED │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ARRAY DESTRUCTURING OBJECT DESTRUCTURING │ +│ ─────────────────── ──────────────────── │ +│ │ +│ const [a, b, c] = [1, 2, 3] const {x, y} = {x: 10, y: 20} │ +│ │ +│ [1, 2, 3] { x: 10, y: 20 } │ +│ │ │ │ │ │ │ +│ │ │ └──► c = 3 │ └──► y = 20 │ +│ │ └─────► b = 2 └──────────► x = 10 │ +│ └────────► a = 1 │ +│ │ +│ Position matters! Property name matters! │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Spread and Rest Operators + +The `...` syntax does two different things depending on context: + +| Context | Name | What It Does | +|---------|------|--------------| +| Function call, array/object literal | **Spread** | Expands an iterable into individual elements | +| Function parameter, destructuring | **Rest** | Collects multiple elements into an array | + +### Spread Operator + +**Spreading arrays:** + +```javascript +const arr1 = [1, 2, 3]; +const arr2 = [4, 5, 6]; + +// Combine arrays +const combined = [...arr1, ...arr2]; +console.log(combined); // [1, 2, 3, 4, 5, 6] + +// Copy an array +const copy = [...arr1]; +console.log(copy); // [1, 2, 3] + +// Insert elements +const withMiddle = [0, ...arr1, 4]; +console.log(withMiddle); // [0, 1, 2, 3, 4] + +// Pass array as function arguments +console.log(Math.max(...arr1)); // 3 +``` + +**Spreading objects:** + +```javascript +const defaults = { theme: 'light', fontSize: 14 }; +const userPrefs = { theme: 'dark' }; + +// Merge objects (later properties override earlier) +const settings = { ...defaults, ...userPrefs }; +console.log(settings); // { theme: 'dark', fontSize: 14 } + +// Copy and update +const updated = { ...user, name: 'Bob' }; + +// Copy an object (shallow!) +const copy = { ...original }; +``` + +### Rest Parameters + +```javascript +// Collect all arguments into an array +function sum(...numbers) { + return numbers.reduce((total, n) => total + n, 0); +} +console.log(sum(1, 2, 3, 4)); // 10 + +// Collect remaining arguments +function logFirst(first, ...rest) { + console.log('First:', first); + console.log('Rest:', rest); +} +logFirst('a', 'b', 'c', 'd'); +// First: a +// Rest: ['b', 'c', 'd'] +``` + +**Rest in destructuring:** + +```javascript +// Arrays +const [first, second, ...others] = [1, 2, 3, 4, 5]; +console.log(others); // [3, 4, 5] + +// Objects +const { id, ...otherProps } = { id: 1, name: 'Alice', age: 25 }; +console.log(otherProps); // { name: 'Alice', age: 25 } +``` + +### The Shallow Copy Trap + +Spread creates **shallow copies**. Nested objects are still referenced: + +```javascript +const original = { + name: 'Alice', + address: { city: 'Portland' } +}; + +const copy = { ...original }; + +// Modifying nested object affects both! +copy.address.city = 'Seattle'; +console.log(original.address.city); // "Seattle" — oops! + +// For deep copies, use structuredClone (modern) or JSON (with limitations) +const deepCopy = structuredClone(original); +``` + +--- + +## Template Literals + +[Template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals) use backticks (`` ` ``) instead of quotes and support string interpolation and multi-line strings. + +### Basic Interpolation + +```javascript +const name = 'Alice'; +const age = 25; + +// Old way +const message = 'Hello, ' + name + '! You are ' + age + ' years old.'; + +// Template literal +const message = `Hello, ${name}! You are ${age} years old.`; + +// Expressions work too +const price = 19.99; +const tax = 0.1; +const total = `Total: $${(price * (1 + tax)).toFixed(2)}`; +console.log(total); // "Total: $21.99" +``` + +### Multi-line Strings + +```javascript +// Old way (awkward) +const html = '<div>\n' + + ' <h1>Title</h1>\n' + + ' <p>Content</p>\n' + + '</div>'; + +// Template literal (natural) +const html = ` + <div> + <h1>${title}</h1> + <p>${content}</p> + </div> +`; +``` + +### Tagged Templates + +Tagged templates let you process template literals with a function: + +```javascript +function highlight(strings, ...values) { + return strings.reduce((result, str, i) => { + const value = values[i] ? `<mark>${values[i]}</mark>` : ''; + return result + str + value; + }, ''); +} + +const query = 'JavaScript'; +const count = 42; + +const result = highlight`Found ${count} results for ${query}`; +console.log(result); +// "Found <mark>42</mark> results for <mark>JavaScript</mark>" +``` + +Tagged templates power libraries like [styled-components](https://styled-components.com/) (CSS-in-JS) and [GraphQL](https://graphql.org/) query builders. + +--- + +## Optional Chaining (`?.`) + +[Optional chaining](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining) lets you safely access nested properties without checking each level for null or undefined. + +### The Problem It Solves + +```javascript +const user = { + name: 'Alice', + // address is undefined +}; + +// Old way (verbose and error-prone) +const city = user && user.address && user.address.city; + +// Old way (slightly better) +const city = user.address ? user.address.city : undefined; + +// Modern way +const city = user?.address?.city; // undefined (no error!) +``` + +### Three Syntax Forms + +```javascript +// Property access +const city = user?.address?.city; + +// Bracket notation (for dynamic keys) +const prop = 'address'; +const value = user?.[prop]?.city; + +// Function calls (only call if function exists) +const result = user?.getName?.(); +``` + +### Short-Circuit Behavior + +When the left side is `null` or `undefined`, evaluation stops immediately and returns `undefined`: + +```javascript +const user = null; + +// Without optional chaining +user.address.city; // TypeError: Cannot read property 'address' of null + +// With optional chaining +user?.address?.city; // undefined (evaluation stops at user) +``` + +### Don't Overuse It + +```javascript +// ❌ BAD - if user should always exist, you're hiding bugs +function processUser(user) { + return user?.name?.toUpperCase(); // Silently returns undefined +} + +// ✓ GOOD - fail fast when data is invalid +function processUser(user) { + if (!user) throw new Error('User is required'); + return user.name.toUpperCase(); +} + +// ✓ GOOD - use when null/undefined is a valid possibility +const displayName = apiResponse?.data?.user?.displayName ?? 'Anonymous'; +``` + +--- + +## Nullish Coalescing (`??`) + +The [nullish coalescing operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing) returns the right-hand side when the left-hand side is `null` or `undefined`. This is different from `||`, which returns the right-hand side for any falsy value. + +### `??` vs `||` + +| Value | `value \|\| 'default'` | `value ?? 'default'` | +|-------|----------------------|---------------------| +| `null` | `'default'` | `'default'` | +| `undefined` | `'default'` | `'default'` | +| `0` | `'default'` | `0` | +| `''` | `'default'` | `''` | +| `false` | `'default'` | `false` | +| `NaN` | `'default'` | `NaN` | + +```javascript +// Problem with || +const count = response.count || 10; +// If response.count is 0, this incorrectly returns 10! + +// Solution with ?? +const count = response.count ?? 10; +// Only returns 10 if count is null or undefined +// Returns 0 if count is 0 (which is what we want) + +// Common use cases +const port = process.env.PORT ?? 3000; +const username = inputValue ?? 'guest'; +const timeout = options.timeout ?? 5000; +``` + +### Combining with Optional Chaining + +These two operators work great together: + +```javascript +const city = user?.address?.city ?? 'Unknown'; +const count = response?.data?.items?.length ?? 0; +``` + +### Logical Assignment Operators + +ES2021 added assignment versions of logical operators: + +```javascript +// Nullish coalescing assignment +user.name ??= 'Anonymous'; +// Only assigns if user.name is null or undefined +// (short-circuits: skips assignment if value already exists) + +// Logical OR assignment +options.debug ||= false; +// Only assigns if options.debug is falsy + +// Logical AND assignment +user.lastLogin &&= new Date(); +// Only assigns if user.lastLogin is truthy +``` + +```javascript +// Practical example: initializing config +function configure(options = {}) { + options.retries ??= 3; + options.timeout ??= 5000; + options.cache ??= true; + return options; +} + +configure({}); // { retries: 3, timeout: 5000, cache: true } +configure({ retries: 0 }); // { retries: 0, timeout: 5000, cache: true } +configure({ timeout: null }); // { retries: 3, timeout: 5000, cache: true } +``` + +--- + +## Default Parameters + +[Default parameters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Default_parameters) let you specify fallback values for function arguments. + +```javascript +// Old way +function greet(name, greeting) { + name = name || 'Guest'; + greeting = greeting || 'Hello'; + return `${greeting}, ${name}!`; +} + +// Modern way +function greet(name = 'Guest', greeting = 'Hello') { + return `${greeting}, ${name}!`; +} + +greet(); // "Hello, Guest!" +greet('Alice'); // "Hello, Alice!" +greet('Alice', 'Hi'); // "Hi, Alice!" +``` + +### Only `undefined` Triggers Defaults + +```javascript +function example(value = 'default') { + return value; +} + +example(undefined); // "default" +example(null); // null (NOT "default"!) +example(0); // 0 +example(''); // '' +example(false); // false +``` + +### Defaults Can Reference Earlier Parameters + +```javascript +function createRect(width, height = width) { + return { width, height }; +} + +createRect(10); // { width: 10, height: 10 } +createRect(10, 20); // { width: 10, height: 20 } +``` + +### Defaults Can Be Expressions + +```javascript +function createId(prefix = 'id', timestamp = Date.now()) { + return `${prefix}_${timestamp}`; +} + +// Date.now() is called each time (not once at definition) +createId(); // "id_1704067200000" +createId(); // "id_1704067200001" (different!) +``` + +--- + +## Enhanced Object Literals + +ES6 added several shortcuts for creating objects. + +### Property Shorthand + +When the property name matches the variable name: + +```javascript +const name = 'Alice'; +const age = 25; + +// Old way +const user = { name: name, age: age }; + +// Shorthand +const user = { name, age }; +console.log(user); // { name: 'Alice', age: 25 } +``` + +### Method Shorthand + +```javascript +// Old way +const calculator = { + add: function(a, b) { + return a + b; + } +}; + +// Shorthand +const calculator = { + add(a, b) { + return a + b; + }, + + // Works with async too + async fetchData(url) { + const response = await fetch(url); + return response.json(); + } +}; +``` + +### Computed Property Names + +Use expressions as property names: + +```javascript +const key = 'dynamicKey'; +const index = 0; + +const obj = { + [key]: 'value', + [`item_${index}`]: 'first item', + ['get' + 'Name']() { + return this.name; + } +}; + +console.log(obj.dynamicKey); // "value" +console.log(obj.item_0); // "first item" +``` + +**Practical example:** + +```javascript +function createState(key, value) { + return { + [key]: value, + [`set${key.charAt(0).toUpperCase() + key.slice(1)}`](newValue) { + this[key] = newValue; + } + }; +} + +const state = createState('count', 0); +console.log(state); // { count: 0, setCount: [Function] } +state.setCount(5); +console.log(state.count); // 5 +``` + +--- + +## Map, Set, and Symbol + +ES6 introduced new built-in data structures and a new primitive type. + +### Map + +[Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) is a collection of key-value pairs where keys can be any type (not just strings). + +```javascript +const map = new Map(); + +// Any value can be a key +const objKey = { id: 1 }; +map.set('string', 'value1'); +map.set(42, 'value2'); +map.set(objKey, 'value3'); + +console.log(map.get(objKey)); // "value3" +console.log(map.size); // 3 +console.log(map.has('string')); // true + +// Iteration (maintains insertion order) +for (const [key, value] of map) { + console.log(key, value); +} + +// Convert to/from arrays +const arr = [...map]; // [['string', 'value1'], [42, 'value2'], ...] +const map2 = new Map([['a', 1], ['b', 2]]); +``` + +**When to use Map vs Object:** + +| Use Case | Object | Map | +|----------|--------|-----| +| Keys are strings | ✓ | ✓ | +| Keys are any type | ✗ | ✓ | +| Need insertion order | ✓ (string keys) | ✓ | +| Need size property | ✗ | ✓ | +| Frequent add/remove | Slower | Faster | +| JSON serialization | ✓ | ✗ | + +### Set + +[Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) is a collection of unique values. + +```javascript +const set = new Set([1, 2, 3, 3, 3]); +console.log(set); // Set { 1, 2, 3 } + +set.add(4); +set.delete(1); +console.log(set.has(2)); // true +console.log(set.size); // 3 + +// Remove duplicates from array +const numbers = [1, 2, 2, 3, 3, 3]; +const unique = [...new Set(numbers)]; +console.log(unique); // [1, 2, 3] + +// Set operations +const a = new Set([1, 2, 3]); +const b = new Set([2, 3, 4]); + +const union = new Set([...a, ...b]); // {1, 2, 3, 4} +const intersection = [...a].filter(x => b.has(x)); // [2, 3] +const difference = [...a].filter(x => !b.has(x)); // [1] +``` + +### Symbol + +[Symbol](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol) is a primitive type for unique identifiers. + +```javascript +// Every Symbol is unique +const sym1 = Symbol('description'); +const sym2 = Symbol('description'); +console.log(sym1 === sym2); // false + +// Use as object keys (hidden from normal iteration) +const ID = Symbol('id'); +const user = { + name: 'Alice', + [ID]: 12345 +}; + +console.log(user[ID]); // 12345 +console.log(Object.keys(user)); // ['name'] (Symbol not included) + +// Well-known Symbols customize object behavior +const collection = { + items: [1, 2, 3], + [Symbol.iterator]() { + let i = 0; + return { + next: () => ({ + value: this.items[i], + done: i++ >= this.items.length + }) + }; + } +}; + +for (const item of collection) { + console.log(item); // 1, 2, 3 +} +``` + +--- + +## for...of Loop + +The [for...of](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of) loop iterates over iterable objects (arrays, strings, Maps, Sets, etc.). + +```javascript +// Arrays +const colors = ['red', 'green', 'blue']; +for (const color of colors) { + console.log(color); // "red", "green", "blue" +} + +// Strings +for (const char of 'hello') { + console.log(char); // "h", "e", "l", "l", "o" +} + +// Maps +const map = new Map([['a', 1], ['b', 2]]); +for (const [key, value] of map) { + console.log(key, value); // "a" 1, "b" 2 +} + +// Sets +const set = new Set([1, 2, 3]); +for (const num of set) { + console.log(num); // 1, 2, 3 +} + +// With destructuring +const users = [ + { name: 'Alice', age: 25 }, + { name: 'Bob', age: 30 } +]; +for (const { name, age } of users) { + console.log(`${name} is ${age}`); +} +``` + +### for...of vs for...in + +| | `for...of` | `for...in` | +|---|-----------|-----------| +| Iterates over | Values | Keys (property names) | +| Works with | Iterables (Array, String, Map, Set) | Objects | +| Array indices | Use `.entries()` | Yes (as strings) | + +```javascript +const arr = ['a', 'b', 'c']; + +for (const value of arr) { + console.log(value); // "a", "b", "c" (values) +} + +for (const index in arr) { + console.log(index); // "0", "1", "2" (keys as strings) +} +``` + +--- + +## Key Takeaways + +<Info> +**The key things to remember about modern JavaScript syntax:** + +1. **Arrow functions inherit `this`** from the enclosing scope. Don't use them as object methods or constructors. + +2. **Destructuring extracts values** from arrays (by position) and objects (by property name). Use it for cleaner function parameters. + +3. **Spread (`...`) expands**, rest (`...`) collects. Same syntax, different contexts. + +4. **`??` checks for null/undefined only**. Use it when `0`, `''`, or `false` are valid values. Use `||` when you want fallback for any falsy value. + +5. **Optional chaining (`?.`)** prevents "cannot read property of undefined" errors. Don't overuse it or you'll hide bugs. + +6. **Template literals** use backticks and support `${expressions}` and multi-line strings. + +7. **Default parameters trigger only on `undefined`**, not `null` or other falsy values. + +8. **Map keys can be any type**, maintain insertion order, and have a `.size` property. Use Map when Object doesn't fit. + +9. **Set stores unique values**. Spread a Set to deduplicate an array: `[...new Set(arr)]`. + +10. **`for...of` iterates values**, `for...in` iterates keys. Use `for...of` for arrays. +</Info> + +--- + +## Test Your Knowledge + +<AccordionGroup> + <Accordion title="Question 1: What's the output of `0 ?? 'default'` vs `0 || 'default'`?"> + **Answer:** + + - `0 ?? 'default'` returns `0` + - `0 || 'default'` returns `'default'` + + The nullish coalescing operator (`??`) only returns the right side for `null` or `undefined`. Since `0` is neither, it returns `0`. + + The logical OR (`||`) returns the right side for any falsy value. Since `0` is falsy, it returns `'default'`. + + ```javascript + // Use ?? when 0 is a valid value + const count = response.count ?? 10; + + // Use || when any falsy value should trigger default + const name = input || 'Anonymous'; + ``` + </Accordion> + + <Accordion title="Question 2: How do you return an object literal from an arrow function?"> + **Answer:** + + Wrap the object literal in parentheses: + + ```javascript + // ❌ WRONG - braces interpreted as function body + const createUser = name => { name, active: true }; + // Returns undefined + + // ✓ CORRECT - parentheses make it an expression + const createUser = name => ({ name, active: true }); + // Returns { name: '...', active: true } + ``` + + Without parentheses, JavaScript interprets `{ }` as a function body block, not an object literal. The parentheses force it to be treated as an expression. + </Accordion> + + <Accordion title="Question 3: What's the difference between spread and rest?"> + **Answer:** + + They use the same `...` syntax but do opposite things: + + **Spread** expands an iterable into individual elements: + ```javascript + const arr = [1, 2, 3]; + console.log(...arr); // 1 2 3 (individual values) + const copy = [...arr]; // [1, 2, 3] (new array) + Math.max(...arr); // 3 (arguments spread) + ``` + + **Rest** collects multiple elements into an array: + ```javascript + function sum(...numbers) { // Collects all args + return numbers.reduce((a, b) => a + b, 0); + } + + const [first, ...rest] = [1, 2, 3, 4]; + // first = 1, rest = [2, 3, 4] + ``` + + **Rule of thumb:** In a function definition or destructuring pattern, it's rest. Everywhere else (function calls, array/object literals), it's spread. + </Accordion> + + <Accordion title="Question 4: Why shouldn't you use arrow functions as object methods?"> + **Answer:** + + Arrow functions don't have their own `this`. They inherit `this` from the enclosing lexical scope, which is usually the global object or `undefined` (in strict mode). + + ```javascript + const user = { + name: 'Alice', + + // ❌ Arrow function - 'this' is NOT the user object + greetArrow: () => { + console.log(`Hi, I'm ${this.name}`); + }, + + // ✓ Regular function - 'this' IS the user object + greetRegular() { + console.log(`Hi, I'm ${this.name}`); + } + }; + + user.greetArrow(); // "Hi, I'm undefined" + user.greetRegular(); // "Hi, I'm Alice" + ``` + + Use regular functions (or method shorthand) for object methods when you need access to `this`. + </Accordion> + + <Accordion title="Question 5: How do you swap two variables without a temporary variable?"> + **Answer:** + + Use array destructuring: + + ```javascript + let a = 1; + let b = 2; + + [a, b] = [b, a]; + + console.log(a); // 2 + console.log(b); // 1 + ``` + + This creates a temporary array `[b, a]` (which is `[2, 1]`), then destructures it back into `a` and `b` in the new order. + </Accordion> + + <Accordion title="Question 6: What does `user?.address?.city ?? 'Unknown'` return if user is null?"> + **Answer:** + + It returns `'Unknown'`. + + Here's the evaluation: + 1. `user?.address` — `user` is `null`, so optional chaining short-circuits and returns `undefined` + 2. `undefined?.city` — This never runs because we already got `undefined` + 3. `undefined ?? 'Unknown'` — `undefined` is nullish, so we get `'Unknown'` + + ```javascript + const user = null; + const city = user?.address?.city ?? 'Unknown'; + console.log(city); // "Unknown" + + // Without optional chaining, this would throw: + // TypeError: Cannot read property 'address' of null + ``` + </Accordion> +</AccordionGroup> + +--- + +## Related Concepts + +<CardGroup cols={2}> + <Card title="Scope and Closures" icon="layer-group" href="/concepts/scope-and-closures"> + Deep dive into let, const, var, block scope, and the temporal dead zone + </Card> + <Card title="this, call, apply and bind" icon="bullseye" href="/concepts/this-call-apply-bind"> + Understanding arrow function this binding in context of all binding rules + </Card> + <Card title="ES Modules" icon="box" href="/concepts/es-modules"> + Modern import/export syntax for organizing JavaScript code + </Card> + <Card title="Promises" icon="handshake" href="/concepts/promises"> + Async features that pair well with modern syntax like async/await + </Card> +</CardGroup> + +--- + +## Reference + +<CardGroup cols={2}> + <Card title="Destructuring Assignment — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment"> + Complete reference for array and object destructuring patterns + </Card> + <Card title="Spread Syntax — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax"> + Documentation for the spread operator in arrays, objects, and function calls + </Card> + <Card title="Arrow Functions — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions"> + Arrow function syntax, limitations, and this binding behavior + </Card> + <Card title="Optional Chaining — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining"> + Safe property access with the ?. operator + </Card> + <Card title="Nullish Coalescing — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing"> + The ?? operator and how it differs from || + </Card> + <Card title="Template Literals — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals"> + String interpolation, multi-line strings, and tagged templates + </Card> +</CardGroup> + +## Articles + +<CardGroup cols={2}> + <Card title="Destructuring Assignment" icon="newspaper" href="https://javascript.info/destructuring-assignment"> + Thorough breakdown of both array and object destructuring with progressive examples from basic to nested patterns. Includes interactive exercises that reinforce each concept. + </Card> + <Card title="Rest Parameters and Spread Syntax" icon="newspaper" href="https://javascript.info/rest-parameters-spread"> + Clearly distinguishes between the visually identical `...` syntax for rest vs spread. The comparison with the legacy `arguments` object shows why modern features are preferred. + </Card> + <Card title="Optional Chaining" icon="newspaper" href="https://javascript.info/optional-chaining"> + Walks through the evolution from verbose `&&` chains to elegant optional chaining, covering all three syntax forms. Includes guidance on when NOT to overuse it. + </Card> + <Card title="Nullish Coalescing Operator" icon="newspaper" href="https://javascript.info/nullish-coalescing-operator"> + Explains the crucial difference between `??` and `||`. This distinction prevents common bugs when working with legitimate zero or empty string values. + </Card> + <Card title="A Dead Simple Intro to Destructuring" icon="newspaper" href="https://wesbos.com/destructuring-objects"> + Wes Bos's practical teaching style with real-world examples including API response handling and deeply nested data extraction. Short, focused, and immediately applicable. + </Card> + <Card title="Template Literals" icon="newspaper" href="https://css-tricks.com/template-literals/"> + Goes beyond basic interpolation to explore tagged template literals for building custom DSLs and sanitizing user input. Includes a practical reusable template function. + </Card> +</CardGroup> + +## Videos + +<CardGroup cols={2}> + <Card title="JavaScript Destructuring in 100 Seconds" icon="video" href="https://www.youtube.com/watch?v=UgEaJBz3bjY"> + Fireship's rapid-fire format packs array destructuring, object destructuring, default values, and nested patterns into a dense but digestible 100 seconds. + </Card> + <Card title="JavaScript ES6 Arrow Functions Tutorial" icon="video" href="https://www.youtube.com/watch?v=h33Srr5J9nY"> + Kyle from Web Dev Simplified walks through arrow function syntax variations, implicit returns, and the critical `this` binding differences from traditional functions. + </Card> + <Card title="Spread Operator and Rest Parameters" icon="video" href="https://www.youtube.com/watch?v=iLx4ma8ZqvQ"> + Practical use cases including array concatenation, object merging, and function argument collection with side-by-side comparisons to ES5 alternatives. + </Card> + <Card title="Optional Chaining Explained" icon="video" href="https://www.youtube.com/watch?v=v2tJ3nzXh8I"> + Shows how optional chaining eliminates defensive coding patterns when accessing deeply nested object properties. Includes real-world API response examples. + </Card> +</CardGroup> diff --git a/docs/concepts/object-creation-prototypes.mdx b/docs/concepts/object-creation-prototypes.mdx new file mode 100644 index 00000000..cb4d2517 --- /dev/null +++ b/docs/concepts/object-creation-prototypes.mdx @@ -0,0 +1,1128 @@ +--- +title: "Object Creation & Prototypes: How Objects Inherit in JavaScript" +sidebarTitle: "Object Creation & Prototypes: How Objects Inherit" +description: "Learn JavaScript's prototype chain and object creation. Understand how inheritance works, the new operator's 4 steps, Object.create(), Object.assign(), and prototype methods." +--- + +How does a plain JavaScript object know about methods like `.toString()` or `.hasOwnProperty()` that you never defined? How does JavaScript let objects inherit from other objects without traditional classes? + +```javascript +// You create a simple object +const player = { name: "Alice", health: 100 } + +// But it has methods you never defined! +console.log(player.toString()) // "[object Object]" +console.log(player.hasOwnProperty("name")) // true + +// Where do these come from? +console.log(Object.getPrototypeOf(player)) // { constructor: Object, toString: f, ... } +``` + +The answer is the **[prototype chain](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain)**. It's JavaScript's inheritance mechanism. Every object has a hidden link to another object called its **prototype**. When you access a property, JavaScript looks for it on the object first, then follows this chain of prototypes until it finds the property or reaches the end (`null`). + +<Info> +**What you'll learn in this guide:** +- What the prototype chain is and how property lookup works +- The difference between `[[Prototype]]`, `__proto__`, and `.prototype` +- How to create objects with `Object.create()` +- What the `new` operator does (the 4 steps) +- How to copy properties with `Object.assign()` +- How to inspect and modify prototypes +- Common prototype methods like `hasOwnProperty()` +- Prototype pitfalls and how to avoid them +</Info> + +<Warning> +**Prerequisites:** This guide assumes you understand [Primitive Types](/concepts/primitive-types) and [Value vs Reference Types](/concepts/value-reference-types). If objects and their properties are new to you, read those guides first! +</Warning> + +--- + +## What is the Prototype Chain? + +The **prototype chain** is JavaScript's way of implementing inheritance. Every object has an internal link (called `[[Prototype]]`) to another object, its prototype. When you try to access a property on an object, JavaScript: + +1. First looks for the property on the object itself +2. If not found, looks on the object's prototype +3. If still not found, looks on the prototype's prototype +4. Continues until it finds the property or reaches `null` (the end of the chain) + +```javascript +// Create a simple object +const wizard = { + name: "Gandalf", + castSpell() { + return `${this.name} casts a spell!` + } +} + +// Create another object that inherits from wizard +const apprentice = Object.create(wizard) +apprentice.name = "Harry" + +// apprentice has its own 'name' property +console.log(apprentice.name) // "Harry" + +// But castSpell comes from the prototype (wizard) +console.log(apprentice.castSpell()) // "Harry casts a spell!" + +// The prototype chain: +// apprentice → wizard → Object.prototype → null +``` + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ THE PROTOTYPE CHAIN │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ apprentice.castSpell() │ +│ │ │ +│ │ 1. Does apprentice have castSpell? NO │ +│ ▼ │ +│ ┌──────────────┐ │ +│ │ apprentice │ │ +│ │──────────────│ │ +│ │ name: "Harry"│ │ +│ │ [[Prototype]]│────┐ │ +│ └──────────────┘ │ │ +│ │ 2. Does wizard have castSpell? YES! Use it │ +│ ▼ │ +│ ┌──────────────────┐ │ +│ │ wizard │ │ +│ │──────────────────│ │ +│ │ name: "Gandalf" │ │ +│ │ castSpell: fn │ ◄── Found here! │ +│ │ [[Prototype]] │────┐ │ +│ └──────────────────┘ │ │ +│ │ 3. If not found, keep going... │ +│ ▼ │ +│ ┌────────────────────┐ │ +│ │ Object.prototype │ │ +│ │────────────────────│ │ +│ │ toString: fn │ │ +│ │ hasOwnProperty: fn │ │ +│ │ [[Prototype]] │────┐ │ +│ └────────────────────┘ │ │ +│ │ │ +│ ▼ │ +│ null │ +│ (end of chain) │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +<Tip> +**The Chain Always Ends:** Every prototype chain eventually reaches `Object.prototype`, then `null`. This is why all objects have access to methods like `toString()` and `hasOwnProperty()`. They inherit them from `Object.prototype`. +</Tip> + +--- + +## Understanding `[[Prototype]]`, `__proto__`, and `.prototype` + +These three terms confuse many developers. Let's clarify: + +| Term | What It Is | How to Access | +|------|------------|---------------| +| `[[Prototype]]` | The internal prototype link every object has | Not directly accessible (it's internal) | +| `__proto__` | A getter/setter that exposes `[[Prototype]]` | `obj.__proto__` (deprecated, avoid in production) | +| `.prototype` | A property on **functions** used when creating instances with `new` | `Function.prototype` | + +```javascript +// Every object has [[Prototype]] — an internal link to its prototype +const player = { name: "Alice" } + +// __proto__ exposes [[Prototype]] (deprecated but works) +console.log(player.__proto__ === Object.prototype) // true + +// .prototype exists only on FUNCTIONS +function Player(name) { + this.name = name +} + +// When you use 'new Player()', the new object's [[Prototype]] +// is set to Player.prototype +console.log(Player.prototype) // { constructor: Player } + +const alice = new Player("Alice") +console.log(Object.getPrototypeOf(alice) === Player.prototype) // true +``` + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ THE THREE PROTOTYPE TERMS │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ [[Prototype]] The hidden internal slot every object has │ +│ ────────────── Points to the object's prototype │ +│ You can't access it directly │ +│ │ +│ __proto__ A way to READ/WRITE [[Prototype]] │ +│ ───────── obj.__proto__ = Object.getPrototypeOf(obj) │ +│ DEPRECATED! Use Object.getPrototypeOf() instead │ +│ │ +│ .prototype A property that exists ONLY on functions │ +│ ────────── Used as the [[Prototype]] for objects │ +│ created with new │ +│ │ +│ ───────────────────────────────────────────────────────────────────── │ +│ │ +│ function Player(name) { this.name = name } │ +│ │ +│ Player.prototype ─────────────┐ │ +│ │ │ +│ const p = new Player("A") │ │ +│ │ │ │ +│ │ [[Prototype]] ════════╧═══▶ { constructor: Player } │ +│ │ │ │ +│ ▼ │ [[Prototype]] │ +│ ┌───────────┐ ▼ │ +│ │ p │ Object.prototype │ +│ │───────────│ │ │ +│ │name: "A" │ │ [[Prototype]] │ +│ └───────────┘ ▼ │ +│ null │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +<Warning> +**Don't use `__proto__` in production code!** It's deprecated and has performance issues. Use `Object.getPrototypeOf()` to read and `Object.setPrototypeOf()` to write (sparingly). +</Warning> + +--- + +## How Property Lookup Works + +When you access a property on an object, JavaScript performs a **prototype chain lookup**: + +<Steps> + <Step title="Check the object itself"> + JavaScript first looks for the property directly on the object. + </Step> + <Step title="Check the prototype"> + If not found, it looks at `Object.getPrototypeOf(obj)` (the object's prototype). + </Step> + <Step title="Continue up the chain"> + If still not found, it checks the prototype's prototype, and so on. + </Step> + <Step title="Reach null or find the property"> + The search stops when the property is found OR when `null` is reached (property is `undefined`). + </Step> +</Steps> + +```javascript +const grandparent = { + familyName: "Smith", + sayHello() { + return `Hello from the ${this.familyName} family!` + } +} + +const parent = Object.create(grandparent) +parent.job = "Engineer" + +const child = Object.create(parent) +child.name = "Alice" + +// Property lookup in action: +console.log(child.name) // "Alice" (found on child) +console.log(child.job) // "Engineer" (found on parent) +console.log(child.familyName) // "Smith" (found on grandparent) +console.log(child.sayHello()) // "Hello from the Smith family!" +console.log(child.age) // undefined (not found anywhere) + +// Visualizing the chain +console.log(Object.getPrototypeOf(child) === parent) // true +console.log(Object.getPrototypeOf(parent) === grandparent) // true +console.log(Object.getPrototypeOf(grandparent) === Object.prototype) // true +console.log(Object.getPrototypeOf(Object.prototype)) // null +``` + +### Property Shadowing + +When you set a property on an object, it creates or updates the property **on that object**, even if a property with the same name exists on the prototype: + +```javascript +const prototype = { + greeting: "Hello", + count: 0 +} + +const obj = Object.create(prototype) + +// Reading — uses prototype's value +console.log(obj.greeting) // "Hello" (from prototype) +console.log(obj.count) // 0 (from prototype) + +// Writing — creates property on obj, "shadows" the prototype's +obj.greeting = "Hi" +obj.count = 5 + +console.log(obj.greeting) // "Hi" (own property) +console.log(prototype.greeting) // "Hello" (unchanged!) + +console.log(obj.count) // 5 (own property) +console.log(prototype.count) // 0 (unchanged!) + +// Check what's "own" vs inherited +console.log(obj.hasOwnProperty("greeting")) // true (it's on obj now) +console.log(obj.hasOwnProperty("count")) // true +``` + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ PROPERTY SHADOWING │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ BEFORE obj.greeting = "Hi" AFTER obj.greeting = "Hi" │ +│ ────────────────────────── ───────────────────────── │ +│ │ +│ obj obj │ +│ ┌─────────────┐ ┌──────────────────┐ │ +│ │ (empty) │ │ greeting: "Hi" │ ◄── shadows │ +│ │ [[Proto]]───┼──┐ │ [[Proto]]────────┼──┐ │ +│ └─────────────┘ │ └──────────────────┘ │ │ +│ │ │ │ +│ ▼ ▼ │ +│ prototype prototype │ +│ ┌──────────────────────┐ ┌──────────────────────┐ │ +│ │ greeting: "Hello" │ │ greeting: "Hello" │ hidden │ +│ │ count: 0 │ │ count: 0 │ │ +│ └──────────────────────┘ └──────────────────────┘ │ +│ │ +│ obj.greeting returns "Hello" obj.greeting returns "Hi" │ +│ (found on prototype) (found on obj, stops looking) │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Ways to Create Objects in JavaScript + +JavaScript gives you several ways to create objects, each with different use cases: + +### 1. Object Literals + +The simplest way. Great for one-off objects: + +```javascript +// Object literal — prototype is automatically Object.prototype +const player = { + name: "Alice", + health: 100, + attack() { + return `${this.name} attacks!` + } +} + +console.log(Object.getPrototypeOf(player) === Object.prototype) // true +``` + +### 2. Object.create() — Create with Specific Prototype + +[`Object.create()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create) creates a new object with a specified prototype: + +```javascript +// Create a prototype object +const animalProto = { + speak() { + return `${this.name} makes a sound.` + }, + eat(food) { + return `${this.name} eats ${food}.` + } +} + +// Create objects that inherit from animalProto +const dog = Object.create(animalProto) +dog.name = "Rex" +dog.breed = "German Shepherd" + +const cat = Object.create(animalProto) +cat.name = "Whiskers" +cat.color = "orange" + +console.log(dog.speak()) // "Rex makes a sound." +console.log(cat.eat("fish")) // "Whiskers eats fish." + +// Both share the same prototype +console.log(Object.getPrototypeOf(dog) === animalProto) // true +console.log(Object.getPrototypeOf(cat) === animalProto) // true +``` + +#### Creating Objects with No Prototype + +Pass `null` to create an object with **no prototype**. This is useful for dictionaries: + +```javascript +// Regular object inherits from Object.prototype +const regular = {} +console.log(regular.toString) // [Function: toString] +console.log("toString" in regular) // true + +// Object with null prototype — truly empty +const dict = Object.create(null) +console.log(dict.toString) // undefined +console.log("toString" in dict) // false + +// Useful for safe dictionaries (no inherited properties to collide with) +dict["hasOwnProperty"] = "I can use any key!" +console.log(dict["hasOwnProperty"]) // "I can use any key!" + +// With regular object, this would shadow the method: +const risky = {} +risky["hasOwnProperty"] = "oops" +// risky.hasOwnProperty("x") would now throw an error! +``` + +#### Object.create() with Property Descriptors + +You can define properties with descriptors: + +```javascript +const person = Object.create(Object.prototype, { + name: { + value: "Alice", + writable: true, + enumerable: true, + configurable: true + }, + age: { + value: 30, + writable: false, // Can't change age + enumerable: true, + configurable: false + }, + secret: { + value: "hidden", + enumerable: false // Won't show in for...in or Object.keys() + } +}) + +console.log(person.name) // "Alice" +console.log(person.age) // 30 +person.age = 25 // Silently fails (or throws in strict mode) +console.log(person.age) // Still 30 + +console.log(Object.keys(person)) // ["name", "age"] (no "secret") +``` + +### 3. The `new` Operator — Create from Constructor + +The [`new`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new) operator creates an object from a constructor function. When you call `new Constructor(args)`, JavaScript performs **4 steps**: + +<Steps> + <Step title="Create a new empty object"> + JavaScript creates a fresh object: `const obj = {}` + </Step> + <Step title="Link the prototype"> + Sets `obj`'s `[[Prototype]]` to `Constructor.prototype` (if it's an object). If `Constructor.prototype` is not an object (e.g., a primitive), the new object uses `Object.prototype` instead. + </Step> + <Step title="Execute the constructor"> + Runs the constructor with `this` bound to the new object + </Step> + <Step title="Return the object"> + Returns `obj` (unless the constructor explicitly returns a non-primitive value) + </Step> +</Steps> + +```javascript +// A constructor function +function Player(name, health) { + // Step 3: 'this' is bound to the new object + this.name = name + this.health = health +} + +// Methods go on the prototype (shared by all instances) +Player.prototype.attack = function() { + return `${this.name} attacks!` +} + +// Create instance with 'new' +const alice = new Player("Alice", 100) + +console.log(alice.name) // "Alice" +console.log(alice.attack()) // "Alice attacks!" +console.log(alice instanceof Player) // true +console.log(Object.getPrototypeOf(alice) === Player.prototype) // true +``` + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ WHAT new Player("Alice", 100) DOES │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Step 1: Create a new empty object │ +│ const obj = {} │ +│ │ +│ Step 2: Link the object's prototype to Constructor.prototype │ +│ Object.setPrototypeOf(obj, Player.prototype) │ +│ │ +│ Step 3: Run the constructor with 'this' bound to the new object │ +│ Player.call(obj, "Alice", 100) │ +│ // Now obj.name = "Alice", obj.health = 100 │ +│ │ +│ Step 4: Return the object (unless constructor returns an object) │ +│ return obj │ +│ │ +│ ───────────────────────────────────────────────────────────────────── │ +│ │ +│ RESULT: │ +│ │ +│ Player.prototype │ +│ ┌─────────────────────┐ │ +│ │ attack: function() │◄───── Shared by all instances │ +│ │ constructor: Player │ │ +│ └─────────────────────┘ │ +│ ▲ │ +│ │ [[Prototype]] │ +│ │ │ +│ ┌────────┴────────┐ │ +│ │ alice │ │ +│ │─────────────────│ │ +│ │ name: "Alice" │ │ +│ │ health: 100 │ │ +│ └─────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +#### Simulating `new` + +Here's a function that does what `new` does: + +```javascript +function myNew(Constructor, ...args) { + // Steps 1 & 2: Create object with correct prototype + const obj = Object.create(Constructor.prototype) + + // Step 3: Run constructor with 'this' = obj + const result = Constructor.apply(obj, args) + + // Step 4: Return result if it's a non-primitive, otherwise return obj + // Note: Functions are also objects, so constructors returning functions + // will override the default return as well + return (result !== null && typeof result === 'object') ? result : obj +} + +// These do the same thing: +const player1 = new Player("Alice", 100) +const player2 = myNew(Player, "Bob", 100) + +console.log(player1 instanceof Player) // true +console.log(player2 instanceof Player) // true +``` + +<Note> +**Edge case:** If a constructor returns a function, that function is returned instead of the new object (since functions are objects in JavaScript). This is rare in practice but technically allowed by the spec. +</Note> + +<Warning> +**Don't forget `new`!** Without it, `this` in a constructor refers to the global object (or `undefined` in strict mode), causing bugs. ES6 classes throw an error if you forget `new`, which is safer. +</Warning> + +### 4. Object.assign() — Copy Properties + +[`Object.assign()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) copies enumerable own properties from source objects to a target: + +```javascript +// Basic usage: copy properties to target +const target = { a: 1 } +const source = { b: 2, c: 3 } + +const result = Object.assign(target, source) + +console.log(result) // { a: 1, b: 2, c: 3 } +console.log(target) // { a: 1, b: 2, c: 3 } — target is modified! +console.log(result === target) // true — returns the target +``` + +#### Merging Multiple Objects + +```javascript +const defaults = { theme: "light", fontSize: 14, showSidebar: true } +const userPrefs = { theme: "dark", fontSize: 16 } +const sessionOverrides = { fontSize: 18 } + +// Later sources overwrite earlier ones +const settings = Object.assign({}, defaults, userPrefs, sessionOverrides) + +console.log(settings) +// { theme: "dark", fontSize: 18, showSidebar: true } + +// Original objects are unchanged (because we used {} as target) +console.log(defaults.fontSize) // 14 +``` + +#### Cloning Objects (Shallow) + +```javascript +const original = { name: "Alice", scores: [90, 85, 92] } + +// Shallow clone +const clone = Object.assign({}, original) + +clone.name = "Bob" +console.log(original.name) // "Alice" — primitive copied by value + +clone.scores.push(100) +console.log(original.scores) // [90, 85, 92, 100] — array shared! +``` + +<Warning> +**`Object.assign()` performs a shallow copy!** Nested objects and arrays are copied by reference, not cloned. For deep cloning, use `structuredClone()` or a library like Lodash. + +```javascript +// Deep clone with structuredClone (modern browsers) +const deepClone = structuredClone(original) +deepClone.scores.push(100) +console.log(original.scores) // [90, 85, 92] — unchanged! +``` +</Warning> + +#### Object.assign() Only Copies Own, Enumerable Properties + +```javascript +const proto = { inherited: "from prototype" } +const source = Object.create(proto) +source.own = "my own property" + +Object.defineProperty(source, "hidden", { + value: "non-enumerable", + enumerable: false +}) + +const target = {} +Object.assign(target, source) + +console.log(target.own) // "my own property" — copied +console.log(target.inherited) // undefined — NOT copied (inherited) +console.log(target.hidden) // undefined — NOT copied (non-enumerable) +``` + +--- + +## Inspecting and Modifying Prototypes + +JavaScript provides methods to work with prototypes: + +### Object.getPrototypeOf() — Read the Prototype + +```javascript +const player = { name: "Alice" } + +// Get the prototype +const proto = Object.getPrototypeOf(player) +console.log(proto === Object.prototype) // true + +// Works with any object +function Game() {} +const game = new Game() +console.log(Object.getPrototypeOf(game) === Game.prototype) // true + +// End of the chain +console.log(Object.getPrototypeOf(Object.prototype)) // null +``` + +### Object.setPrototypeOf() — Change the Prototype + +[`Object.setPrototypeOf()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/setPrototypeOf) changes an object's prototype after creation: + +```javascript +const swimmer = { + swim() { return `${this.name} swims!` } +} + +const flyer = { + fly() { return `${this.name} flies!` } +} + +const duck = { name: "Donald" } + +// Start as a swimmer +Object.setPrototypeOf(duck, swimmer) +console.log(duck.swim()) // "Donald swims!" + +// Change to a flyer +Object.setPrototypeOf(duck, flyer) +console.log(duck.fly()) // "Donald flies!" +// console.log(duck.swim()) // TypeError: duck.swim is not a function +``` + +<Warning> +**Avoid `Object.setPrototypeOf()` in performance-critical code!** Changing an object's prototype after creation is slow and can deoptimize your code. Set the prototype correctly at creation time with `Object.create()` instead. +</Warning> + +### instanceof — Check the Prototype Chain + +The [`instanceof`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/instanceof) operator checks if `Constructor.prototype` exists in the object's prototype chain: + +```javascript +function Animal(name) { + this.name = name +} + +function Dog(name, breed) { + Animal.call(this, name) + this.breed = breed +} + +// Set up inheritance +Dog.prototype = Object.create(Animal.prototype) +Dog.prototype.constructor = Dog + +const rex = new Dog("Rex", "German Shepherd") + +console.log(rex instanceof Dog) // true +console.log(rex instanceof Animal) // true +console.log(rex instanceof Object) // true +console.log(rex instanceof Array) // false +``` + +### isPrototypeOf() — Check if Object is in Chain + +```javascript +const animal = { eats: true } +const dog = Object.create(animal) +dog.barks = true + +console.log(animal.isPrototypeOf(dog)) // true +console.log(Object.prototype.isPrototypeOf(dog)) // true +console.log(Array.prototype.isPrototypeOf(dog)) // false +``` + +--- + +## Common Prototype Methods + +These methods help you work with object properties and prototypes: + +### hasOwnProperty() — Check Own Properties + +```javascript +const proto = { inherited: true } +const obj = Object.create(proto) +obj.own = true + +// hasOwnProperty checks ONLY the object, not the chain +console.log(obj.hasOwnProperty("own")) // true +console.log(obj.hasOwnProperty("inherited")) // false + +// 'in' operator checks the whole chain +console.log("own" in obj) // true +console.log("inherited" in obj) // true +``` + +<Tip> +**Modern alternative: `Object.hasOwn()`** (ES2022+) + +Use [`Object.hasOwn()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwn) instead of `hasOwnProperty()`. It's safer because it works on objects with a `null` prototype and can't be shadowed: + +```javascript +// hasOwnProperty can be shadowed or unavailable +const nullProto = Object.create(null) +nullProto.key = "value" +// nullProto.hasOwnProperty("key") // TypeError: not a function + +// Object.hasOwn always works +Object.hasOwn(nullProto, "key") // true +``` +</Tip> + +### Object.keys() vs for...in + +```javascript +const proto = { inherited: "value" } +const obj = Object.create(proto) +obj.own1 = "a" +obj.own2 = "b" + +// Object.keys() — only own enumerable properties +console.log(Object.keys(obj)) // ["own1", "own2"] + +// for...in — own AND inherited enumerable properties +for (const key in obj) { + console.log(key) // "own1", "own2", "inherited" +} + +// Filter for...in to only own properties +for (const key in obj) { + if (obj.hasOwnProperty(key)) { + console.log(key) // "own1", "own2" + } +} +``` + +### Object.getOwnPropertyNames() — All Own Properties + +```javascript +const obj = { visible: true } +Object.defineProperty(obj, "hidden", { + value: "secret", + enumerable: false +}) + +// Object.keys() — only enumerable +console.log(Object.keys(obj)) // ["visible"] + +// Object.getOwnPropertyNames() — all own properties +console.log(Object.getOwnPropertyNames(obj)) // ["visible", "hidden"] +``` + +### Summary Table + +| Method | Own? | Enumerable? | Inherited? | +|--------|------|-------------|------------| +| `obj.hasOwnProperty(key)` | Yes | Both | No | +| `key in obj` | Yes | Both | Yes | +| `Object.keys(obj)` | Yes | Yes only | No | +| `Object.getOwnPropertyNames(obj)` | Yes | Both | No | +| `for...in` | Yes | Yes only | Yes | + +--- + +## The Prototype Pitfall: Common Mistakes + +### Mistake 1: Modifying Object.prototype + +```javascript +// ❌ NEVER do this! +Object.prototype.greet = function() { + return "Hello!" +} + +// Now EVERY object has greet() +const player = { name: "Alice" } +const numbers = [1, 2, 3] +const date = new Date() + +console.log(player.greet()) // "Hello!" +console.log(numbers.greet()) // "Hello!" +console.log(date.greet()) // "Hello!" + +// This can break for...in loops +for (const key in player) { + console.log(key) // "name", "greet" — greet shows up! +} + +// And cause conflicts with libraries +``` + +<Warning> +**Never modify `Object.prototype`!** It affects every object in your application and can break third-party code. If you need to add methods to all objects of a type, create your own constructor or class. +</Warning> + +### Mistake 2: Confusing `.prototype` with `[[Prototype]]` + +```javascript +function Player(name) { + this.name = name +} + +const alice = new Player("Alice") + +// ❌ WRONG — instances don't have .prototype +console.log(alice.prototype) // undefined + +// ✓ CORRECT — use Object.getPrototypeOf() +console.log(Object.getPrototypeOf(alice) === Player.prototype) // true + +// .prototype is ONLY on functions +console.log(Player.prototype) // { constructor: Player } +``` + +### Mistake 3: Prototype Pollution + +[Prototype pollution](https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/Prototype_pollution) occurs when attackers can modify `Object.prototype`, affecting all objects. This is a real security vulnerability: + +```javascript +// ❌ DANGEROUS - merging untrusted data can pollute prototypes +const maliciousPayload = JSON.parse('{"__proto__": {"isAdmin": true}}') + +const user = {} +Object.assign(user, maliciousPayload) // Pollution via Object.assign! + +// Now ALL objects have isAdmin! +const anotherUser = {} +console.log(anotherUser.isAdmin) // true - polluted! + +// ✓ SAFER - use null prototype objects for dictionaries +const safeDict = Object.create(null) +safeDict["__proto__"] = "safe" // Just a regular property, no pollution + +// ✓ SAFEST - use Map for key-value storage with untrusted keys +const map = new Map() +map.set("__proto__", "value") // Completely safe +``` + +<Warning> +**Prototype pollution attacks** can occur through `Object.assign()`, object spread (`{...obj}`), deep merge utilities, and JSON parsing. Always sanitize untrusted input and consider using `Object.create(null)` or `Map` for user-controlled keys. +</Warning> + +### Mistake 4: Shared Reference on Prototype + +```javascript +// ❌ WRONG — array on prototype is shared by all instances +function Player(name) { + this.name = name +} +Player.prototype.inventory = [] // Shared by ALL players! + +const alice = new Player("Alice") +const bob = new Player("Bob") + +alice.inventory.push("sword") +console.log(bob.inventory) // ["sword"] — Bob has Alice's sword! + +// ✓ CORRECT — initialize arrays in constructor +function Player(name) { + this.name = name + this.inventory = [] // Each player gets their own array +} +``` + +--- + +## Key Takeaways + +<Info> +**Key things to remember about prototypes and object creation:** + +1. **Every object has a prototype** — a hidden link (`[[Prototype]]`) to another object, forming a chain that ends at `null` + +2. **Property lookup walks the chain** — JavaScript searches the object first, then its prototype, then the prototype's prototype, and so on + +3. **`[[Prototype]]` vs `.prototype`** — `[[Prototype]]` is the internal link every object has; `.prototype` is a property on functions used with `new` + +4. **Use `Object.getPrototypeOf()`** — not `__proto__`, which is deprecated + +5. **`Object.create(proto)`** — creates an object with a specific prototype; pass `null` for no prototype + +6. **The `new` operator does 4 things** — creates object, links prototype, runs constructor with `this`, returns the object + +7. **`Object.assign()` is shallow** — nested objects are copied by reference, not cloned + +8. **`hasOwnProperty()` vs `in`** — `hasOwnProperty` checks only the object; `in` checks the whole prototype chain + +9. **Never modify `Object.prototype`** — it affects all objects and can break code + +10. **Put methods on the prototype** — for memory efficiency, don't define methods in the constructor +</Info> + +--- + +## Test Your Knowledge + +<AccordionGroup> + <Accordion title="Question 1: What is the prototype chain and how does property lookup work?"> + **Answer:** + + The prototype chain is JavaScript's inheritance mechanism. Every object has a `[[Prototype]]` link to another object (its prototype). + + When you access a property: + 1. JavaScript looks for it on the object itself + 2. If not found, looks on the object's prototype + 3. Continues up the chain until found or `null` is reached + + ```javascript + const parent = { greet: "Hello" } + const child = Object.create(parent) + + console.log(child.greet) // "Hello" — found on prototype + console.log(child.missing) // undefined — not found anywhere + ``` + </Accordion> + + <Accordion title="Question 2: What's the difference between [[Prototype]], __proto__, and .prototype?"> + **Answer:** + + - **`[[Prototype]]`**: The internal slot every object has, pointing to its prototype. Not directly accessible. + + - **`__proto__`**: A deprecated getter/setter that exposes `[[Prototype]]`. Use `Object.getPrototypeOf()` instead. + + - **`.prototype`**: A property that exists **only on functions**. When you use `new`, the created object's `[[Prototype]]` is set to this value. + + ```javascript + function Foo() {} + const f = new Foo() + + // f's [[Prototype]] is Foo.prototype + Object.getPrototypeOf(f) === Foo.prototype // true + + // Foo is a function, so it has .prototype + Foo.prototype // { constructor: Foo } + + // f is NOT a function, so it has no .prototype + f.prototype // undefined + ``` + </Accordion> + + <Accordion title="Question 3: What are the 4 steps the new keyword performs?"> + **Answer:** + + When you call `new Constructor(args)`: + + 1. **Create** a new empty object `{}` + 2. **Link** the object's `[[Prototype]]` to `Constructor.prototype` + 3. **Execute** the constructor with `this` bound to the new object + 4. **Return** the object (unless the constructor returns a different object) + + ```javascript + function myNew(Constructor, ...args) { + const obj = Object.create(Constructor.prototype) // Steps 1-2 + const result = Constructor.apply(obj, args) // Step 3 + return (typeof result === 'object' && result !== null) ? result : obj // Step 4 + } + ``` + </Accordion> + + <Accordion title="Question 4: How does Object.create() differ from using new?"> + **Answer:** + + - **`Object.create(proto)`** creates an object with the specified object as its prototype. It doesn't call any constructor. + + - **`new Constructor()`** creates an object with `Constructor.prototype` as its prototype AND runs the constructor function. + + ```javascript + const proto = { greet() { return "Hi!" } } + + // Object.create — just links the prototype + const obj1 = Object.create(proto) + + // new — links prototype AND runs constructor + function MyClass() { + this.initialized = true + } + MyClass.prototype = proto + + const obj2 = new MyClass() + console.log(obj2.initialized) // true (constructor ran) + console.log(obj1.initialized) // undefined (no constructor) + ``` + </Accordion> + + <Accordion title="Question 5: Why should you avoid modifying Object.prototype?"> + **Answer:** + + Modifying `Object.prototype` affects **every object** in your application because all objects inherit from it. This can: + + 1. Break `for...in` loops (new properties show up) + 2. Conflict with third-party libraries + 3. Cause unexpected behavior throughout your codebase + + ```javascript + // ❌ BAD + Object.prototype.bad = "affects everything" + + const obj = {} + for (const key in obj) { + console.log(key) // "bad" — unexpected! + } + ``` + + Instead, create your own constructors/classes or use composition. + </Accordion> + + <Accordion title="Question 6: What's the difference between Object.assign() shallow copy and deep copy?"> + **Answer:** + + **Shallow copy**: Copies the top-level properties. Nested objects/arrays are copied by reference (they point to the same data). + + **Deep copy**: Recursively copies all levels. Nested objects/arrays are fully cloned. + + ```javascript + const original = { + name: "Alice", + scores: [90, 85] // nested array + } + + // Shallow copy with Object.assign + const shallow = Object.assign({}, original) + shallow.scores.push(100) + console.log(original.scores) // [90, 85, 100] — modified! + + // Deep copy with structuredClone + const deep = structuredClone(original) + deep.scores.push(100) + console.log(original.scores) // [90, 85, 100] — still modified from before + // But if we had deep copied first, original would be unchanged + ``` + </Accordion> +</AccordionGroup> + +--- + +## Related Concepts + +<CardGroup cols={2}> + <Card title="Factories and Classes" icon="industry" href="/concepts/factories-classes"> + Learn different patterns for creating objects using factories and ES6 classes + </Card> + <Card title="this, call, apply, bind" icon="hand-pointer" href="/concepts/this-call-apply-bind"> + Understand how `this` binding works, which is crucial when working with constructors + </Card> + <Card title="Inheritance and Polymorphism" icon="sitemap" href="/concepts/inheritance-polymorphism"> + Explore advanced inheritance patterns and polymorphism in JavaScript + </Card> + <Card title="Value vs Reference Types" icon="copy" href="/concepts/value-reference-types"> + Understand the difference between primitives and objects, key background for prototypes + </Card> +</CardGroup> + +--- + +## Reference + +<CardGroup cols={2}> + <Card title="Inheritance and the Prototype Chain — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain"> + Comprehensive MDN guide to JavaScript's prototype-based inheritance + </Card> + <Card title="Object.create() — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create"> + Official documentation on creating objects with specific prototypes + </Card> + <Card title="Object.assign() — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign"> + How to copy properties between objects + </Card> + <Card title="new operator — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new"> + What happens when you use the new keyword + </Card> + <Card title="Object.getPrototypeOf() — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getPrototypeOf"> + How to read an object's prototype + </Card> + <Card title="instanceof — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/instanceof"> + Checking prototype chain membership + </Card> +</CardGroup> + +## Articles + +<CardGroup cols={2}> + <Card title="A Beginner's Guide to JavaScript's Prototype" icon="newspaper" href="https://www.freecodecamp.org/news/a-beginners-guide-to-javascripts-prototype/"> + Uses a "meal recipe" analogy that makes prototype inheritance click for visual learners. The step-by-step diagrams showing object relationships are particularly helpful. + </Card> + <Card title="Understanding Prototypes in JavaScript" icon="newspaper" href="https://www.digitalocean.com/community/tutorials/understanding-prototypes-and-inheritance-in-javascript"> + Walks through building a full inheritance hierarchy from scratch with runnable examples. Great for developers who learn by building rather than reading theory. + </Card> + <Card title="Object-Oriented JavaScript" icon="newspaper" href="https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Advanced_JavaScript_objects"> + MDN's learning path covering object basics, prototypes, and classes. Includes hands-on exercises and a practical project to solidify your understanding. + </Card> + <Card title="The Prototype Chain Explained" icon="newspaper" href="https://javascript.info/prototype-inheritance"> + Includes interactive code examples you can edit and run in the browser. The "tasks" section at the end tests your understanding with practical challenges. + </Card> +</CardGroup> + +## Videos + +<CardGroup cols={2}> + <Card title="JavaScript Prototypes Explained" icon="video" href="https://www.youtube.com/watch?v=riDVvXZ_Kb4"> + MPJ's signature whiteboard diagrams make the prototype chain visible and intuitive. His "delegation, not copying" explanation is how prototypes finally click for many developers. + </Card> + <Card title="Object.create and Prototypes" icon="video" href="https://www.youtube.com/watch?v=MACDGu96wrA"> + Kyle Simpson (author of "You Don't Know JS") challenges common misconceptions about prototypes. His "behavior delegation" framing offers a clearer mental model than classical inheritance. + </Card> + <Card title="The new Keyword Explained" icon="video" href="https://www.youtube.com/watch?v=Y3zzCY62NYc"> + Steps through each of the 4 things `new` does with live code demonstrations. Shows exactly what happens to `this` and prototype links during object construction. + </Card> +</CardGroup> diff --git a/docs/concepts/primitive-types.mdx b/docs/concepts/primitive-types.mdx new file mode 100644 index 00000000..b9ccd933 --- /dev/null +++ b/docs/concepts/primitive-types.mdx @@ -0,0 +1,1100 @@ +--- +title: "Primitive Types: Building Blocks of Data in JavaScript" +sidebarTitle: "Primitive Types: Building Blocks of Data" +description: "Learn JavaScript's 7 primitive types: string, number, bigint, boolean, undefined, null, and symbol. Understand immutability, typeof quirks, and autoboxing." +--- + +What's the difference between `"hello"` and `{ text: "hello" }`? Why can you call `"hello".toUpperCase()` if strings aren't objects? And why does `typeof null` return `"object"`? + +```javascript +// JavaScript has exactly 7 primitive types +const str = "hello"; // string +const num = 42; // number +const big = 9007199254740993n; // bigint +const bool = true; // boolean +const undef = undefined; // undefined +const nul = null; // null +const sym = Symbol("id"); // symbol + +console.log(typeof str); // "string" +console.log(typeof num); // "number" +console.log(typeof nul); // "object" — Wait, what?! +``` + +These seven **[primitive](https://developer.mozilla.org/en-US/docs/Glossary/Primitive)** types are the foundation of all data in JavaScript. Unlike objects, primitives are **immutable** (unchangeable) and **compared by value**. Every complex structure you build (arrays, objects, classes) ultimately relies on these simple building blocks. + +<Info> +**What you'll learn in this guide:** +- The 7 primitive types in JavaScript and when to use each +- How `typeof` works (and its famous quirks) +- Why primitives are "immutable" and what that means +- The magic of autoboxing — how `"hello".toUpperCase()` works +- The difference between `null` and `undefined` +- Common mistakes to avoid with primitives +- Famous JavaScript gotchas every developer should know +</Info> + +<Note> +**New to JavaScript?** This guide is beginner-friendly! No prior knowledge required. We'll explain everything from the ground up. +</Note> + +--- + +## What Are Primitive Types? + +In JavaScript, a **primitive** is data that is not an object and has no methods of its own. JavaScript has exactly **7 primitive types**: + +| Type | Example | Description | +|------|---------|-------------| +| `string` | `"hello"` | Text data | +| `number` | `42`, `3.14` | Numeric data (integers and decimals) | +| `bigint` | `9007199254740993n` | Very large integers | +| `boolean` | `true`, `false` | Logical values | +| `undefined` | `undefined` | No value assigned | +| `null` | `null` | Intentional absence of value | +| `symbol` | `Symbol("id")` | Unique identifier | + +### Three Key Characteristics + +All primitives share these fundamental traits: + +<AccordionGroup> + <Accordion title="1. Immutable - Values Cannot Be Changed"> + Once a primitive value is created, it cannot be altered. When you "change" a string, you're actually creating a new string. + + ```javascript + let name = "Alice"; + name.toUpperCase(); // Creates "ALICE" but doesn't change 'name' + console.log(name); // Still "Alice" + ``` + </Accordion> + + <Accordion title="2. Compared By Value"> + When you compare two primitives, JavaScript compares their actual values, not where they're stored in memory. + + ```javascript + let a = "hello"; + let b = "hello"; + console.log(a === b); // true - same value + + let obj1 = { text: "hello" }; + let obj2 = { text: "hello" }; + console.log(obj1 === obj2); // false - different objects! + ``` + </Accordion> + + <Accordion title="3. No Methods (But Autoboxing Magic)"> + Primitives don't have methods, but JavaScript automatically wraps them in objects when you try to call methods. This is called "autoboxing." + + ```javascript + "hello".toUpperCase(); // Works! JS wraps "hello" in a String object + ``` + + We'll explore this magic in detail later. + </Accordion> +</AccordionGroup> + +--- + +## The Atoms vs Molecules Analogy + +Think of data in JavaScript like chemistry class (but way more fun, and no lab goggles required). **Primitives** are like atoms: the fundamental, indivisible building blocks that cannot be broken down further. **Objects** are like molecules: complex structures made up of multiple atoms combined together. + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ PRIMITIVES VS OBJECTS │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ PRIMITIVES (Atoms) OBJECTS (Molecules) │ +│ │ +│ ┌───┐ ┌─────┐ ┌──────┐ ┌────────────────────────────┐ │ +│ │ 5 │ │"hi" │ │ true │ │ { name: "Alice", age: 25 } │ │ +│ └───┘ └─────┘ └──────┘ └────────────────────────────┘ │ +│ │ +│ • Simple, indivisible • Complex, contains values │ +│ • Stored directly • Stored as reference │ +│ • Compared by value • Compared by reference │ +│ • Immutable • Mutable │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +Just like atoms are the foundation of all matter, primitives are the foundation of all data in JavaScript. Every complex data structure you create — arrays, objects, functions — is ultimately built on top of these simple primitive values. + +--- + +## The 7 Primitive Types: Deep Dive + +Let's explore each primitive type in detail. + +--- + +### String + +A **[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** represents text data: a sequence of characters. + +```javascript +// Three ways to create strings +let single = 'Hello'; // Single quotes +let double = "World"; // Double quotes +let backtick = `Hello World`; // Template literal (ES6) +``` + +#### Template Literals (Still Just Strings!) + +[Template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals) (backticks) are **not a separate type**. They're just a more powerful syntax for creating strings. The result is still a regular string primitive: + +```javascript +let name = "Alice"; +let age = 25; + +// String interpolation - embed expressions +let greeting = `Hello, ${name}! You are ${age} years old.`; +console.log(greeting); // "Hello, Alice! You are 25 years old." +console.log(typeof greeting); // "string" — it's just a string! + +// Multi-line strings +let multiLine = ` + This is line 1 + This is line 2 +`; +console.log(typeof multiLine); // "string" +``` + +#### Strings Are Immutable + +You cannot change individual characters in a string: + +```javascript +let str = "hello"; +str[0] = "H"; // Does nothing! No error, but no change +console.log(str); // Still "hello" + +// To "change" a string, create a new one +str = "H" + str.slice(1); +console.log(str); // "Hello" +``` + +<Tip> +String methods like `toUpperCase()`, `slice()`, `replace()` always return **new strings**. They never modify the original. +</Tip> + +--- + +### Number + +JavaScript has only **one [number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number) type** for both integers and decimals. All numbers are stored as 64-bit floating-point (a standard way computers store decimals). + +```javascript +let integer = 42; // Integer +let decimal = 3.14; // Decimal +let negative = -10; // Negative +let scientific = 2.5e6; // 2,500,000 (scientific notation) +``` + +#### Special Number Values + +```javascript +console.log(1 / 0); // Infinity +console.log(-1 / 0); // -Infinity +console.log("hello" * 2); // NaN (Not a Number) +``` + +JavaScript has special number values: [`Infinity`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Infinity) for values too large to represent, and [`NaN`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NaN) (Not a Number) for invalid mathematical operations. + +#### The Famous Floating-Point Problem + +```javascript +console.log(0.1 + 0.2); // 0.30000000000000004 +console.log(0.1 + 0.2 === 0.3); // false! Welcome to JavaScript! +``` + +This isn't a JavaScript bug. It's how floating-point math works in all programming languages. The decimal `0.1` cannot be perfectly represented in binary. + +<Warning> +**Working with money?** Never use floating-point for calculations! Store amounts in cents as integers, then use JavaScript's built-in [`Intl.NumberFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat) for display. + +```javascript +// Bad: floating-point errors in calculations +let price = 0.1 + 0.2; // 0.30000000000000004 + +// Good: calculate in cents, format for display +let priceInCents = 10 + 20; // 30 (calculation is accurate!) + +// Use Intl.NumberFormat to display as currency +const formatter = new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', +}); +console.log(formatter.format(priceInCents / 100)); // "$0.30" + +// Works for any locale and currency! +const euroFormatter = new Intl.NumberFormat('de-DE', { + style: 'currency', + currency: 'EUR', +}); +console.log(euroFormatter.format(1234.56)); // "1.234,56 €" +``` +</Warning> + +<Tip> +`Intl.NumberFormat` is built into JavaScript. No external libraries needed! It handles currency symbols, decimal separators, and locale-specific formatting automatically. +</Tip> + +#### Safe Integer Range + +JavaScript can only safely represent integers up to a certain size: + +```javascript +console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991 (2^53 - 1) +console.log(Number.MIN_SAFE_INTEGER); // -9007199254740991 +``` + +[`Number.MAX_SAFE_INTEGER`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER) is the largest integer that can be safely represented. Beyond this range, precision is lost: + +```javascript +// Beyond this range, precision is lost +console.log(9007199254740992 === 9007199254740993); // true! (wrong!) +``` + +For larger integers, use `BigInt`. + +--- + +### BigInt + +**[BigInt](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt)** (ES2020) represents integers larger than `Number.MAX_SAFE_INTEGER`. + +```javascript +// Add 'n' suffix to create a BigInt +let big = 9007199254740993n; +let alsoBig = BigInt("9007199254740993"); + +console.log(big + 1n); // 9007199254740994n (correct!) +``` + +#### BigInt Rules + +```javascript +// Cannot mix BigInt and Number +let big = 10n; +let regular = 5; +// console.log(big + regular); // TypeError! + +// Must convert explicitly +console.log(big + BigInt(regular)); // 15n +console.log(Number(big) + regular); // 15 +``` + +<Note> +**When to use BigInt:** Cryptography, precise timestamps, database IDs, any calculation requiring integers larger than 9 quadrillion. +</Note> + +--- + +### Boolean + +**[Boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** has exactly two values: `true` and `false`. + +```javascript +let isLoggedIn = true; +let hasPermission = false; + +// From comparisons +let isAdult = age >= 18; // true or false +let isEqual = name === "Alice"; // true or false +``` + +#### Truthy and Falsy + +When used in boolean contexts (like `if` statements), all values are either "truthy" or "falsy": + +```javascript +// Falsy values (only 8!) +false +0 +-0 +0n // BigInt zero +"" // Empty string +null +undefined +NaN + +// Everything else is truthy +"hello" // truthy +42 // truthy +[] // truthy (empty array!) +{} // truthy (empty object!) +``` + +```javascript +// Convert any value to boolean +let value = "hello"; +let bool = Boolean(value); // true +let shortcut = !!value; // true (double negation trick) +``` + +<Tip> +Learn more about how JavaScript converts between types in the [Type Coercion](/concepts/type-coercion) section. +</Tip> + +--- + +### undefined + +**[`undefined`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/undefined)** means "no value has been assigned." JavaScript uses it automatically in several situations: + +```javascript +// 1. Declared but not assigned +let x; +console.log(x); // undefined + +// 2. Missing function parameters +function greet(name) { + console.log(name); // undefined if called without argument +} +greet(); + +// 3. Function with no return statement +function doNothing() { + // no return +} +console.log(doNothing()); // undefined + +// 4. Accessing non-existent object property +let person = { name: "Alice" }; +console.log(person.age); // undefined +``` + +<Tip> +Don't explicitly assign `undefined` to variables. Use `null` instead to indicate "intentionally empty." +</Tip> + +--- + +### null + +**[`null`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/null)** means "intentionally empty". You're explicitly saying "this has no value." + +```javascript +// Intentionally clearing a variable +let user = { name: "Alice" }; +user = null; // User logged out, clearing the reference + +// Indicating no result +function findUser(id) { + // ... search logic ... + return null; // User not found +} +``` + +#### The Famous typeof Bug + +```javascript +console.log(typeof null); // "object" — Wait, what?! +``` + +Yes, really. This is one of JavaScript's most famous quirks! It's a historical mistake from JavaScript's first implementation in 1995. It was never fixed because too much existing code depends on it. + +```javascript +// How to properly check for null +let value = null; +console.log(value === null); // true (use strict equality) +``` + +--- + +### Symbol + +**[Symbol](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol)** (ES6) creates unique identifiers. Even symbols with the same description are different. + +```javascript +let id1 = Symbol("id"); +let id2 = Symbol("id"); + +console.log(id1 === id2); // false — always unique! +console.log(id1.description); // "id" (the description) +``` + +#### Use Case: Unique Object Keys + +```javascript +const ID = Symbol("id"); +const user = { + name: "Alice", + [ID]: 12345 // Symbol as property key +}; + +console.log(user.name); // "Alice" +console.log(user[ID]); // 12345 + +// Symbol keys don't appear in normal iteration +console.log(Object.keys(user)); // ["name"] — ID not included +``` + +#### Well-Known Symbols + +JavaScript has built-in symbols for customizing object behavior: + +```javascript +// Symbol.iterator - make an object iterable +// Symbol.toStringTag - customize Object.prototype.toString +// Symbol.toPrimitive - customize type conversion +``` + +These are called [well-known symbols](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol#well-known_symbols) and allow you to customize how objects behave with built-in operations. + +<Note> +Symbols are an advanced feature. As a beginner, focus on understanding that they exist and create unique values. You'll encounter them when diving into advanced patterns and library code. +</Note> + +--- + +## The typeof Operator + +The [`typeof`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof) operator returns a string indicating the type of a value. + +```javascript +console.log(typeof "hello"); // "string" +console.log(typeof 42); // "number" +console.log(typeof 42n); // "bigint" +console.log(typeof true); // "boolean" +console.log(typeof undefined); // "undefined" +console.log(typeof Symbol()); // "symbol" +console.log(typeof null); // "object" ⚠️ (bug!) +console.log(typeof {}); // "object" +console.log(typeof []); // "object" +console.log(typeof function(){}); // "function" +``` + +### typeof Results Table + +| Value | typeof Result | Notes | +|-------|---------------|-------| +| `"hello"` | `"string"` | | +| `42` | `"number"` | | +| `42n` | `"bigint"` | | +| `true` / `false` | `"boolean"` | | +| `undefined` | `"undefined"` | | +| `Symbol()` | `"symbol"` | | +| `null` | `"object"` | Historical bug! | +| `{}` | `"object"` | | +| `[]` | `"object"` | Arrays are objects | +| `function(){}` | `"function"` | Functions are special | + +### Better Type Checking + +Since `typeof` has quirks, here are more reliable alternatives: + +```javascript +// Check for null specifically +let value = null; +if (value === null) { + console.log("It's null"); +} + +// Check for arrays +Array.isArray([1, 2, 3]); // true +Array.isArray("hello"); // false + +// Get precise type with Object.prototype.toString +Object.prototype.toString.call(null); // "[object Null]" +Object.prototype.toString.call([]); // "[object Array]" +Object.prototype.toString.call(new Date()); // "[object Date]" +``` + +[`Array.isArray()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray) is the reliable way to check for arrays, since `typeof []` returns `"object"`. For more complex type checking, [`Object.prototype.toString()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString) gives precise type information. + +--- + +## Immutability Explained + +**Immutable** means "cannot be changed." Primitive values are immutable. You cannot alter the value itself. + +### Seeing Immutability in Action + +```javascript +let str = "hello"; + +// These methods don't change 'str' — they return NEW strings +str.toUpperCase(); // Returns "HELLO" +console.log(str); // Still "hello"! + +// To capture the new value, you must reassign +str = str.toUpperCase(); +console.log(str); // Now "HELLO" +``` + +### Visual: What Happens in Memory + +``` +BEFORE str.toUpperCase(): +┌─────────────────┐ +│ str → "hello" │ (original string) +└─────────────────┘ + +AFTER str.toUpperCase() (without reassignment): +┌─────────────────┐ +│ str → "hello" │ (unchanged!) +└─────────────────┘ +┌─────────────────┐ +│ "HELLO" │ (new string, not captured, garbage collected) +└─────────────────┘ + +AFTER str = str.toUpperCase(): +┌─────────────────┐ +│ str → "HELLO" │ (str now points to new string) +└─────────────────┘ +``` + +### Common Misconception: const vs Immutability + +`const` prevents **reassignment**, not mutation. These are different concepts! + +```javascript +// const prevents reassignment +const name = "Alice"; +// name = "Bob"; // Error! Cannot reassign const + +// But const doesn't make objects immutable +const person = { name: "Alice" }; +person.name = "Bob"; // Works! Mutating the object +person.age = 25; // Works! Adding a property +// person = {}; // Error! Cannot reassign const + +// Primitives are immutable regardless of const/let +let str = "hello"; +str[0] = "H"; // Silently fails — can't mutate primitive +``` + +<Tip> +Think of it this way: `const` protects the **variable** (the container). Immutability protects the **value** (the content). +</Tip> + +--- + +## Autoboxing: The Secret Life of Primitives + +If primitives have no methods, how does `"hello".toUpperCase()` work? + +### The Magic Behind the Scenes + +When you access a property or method on a primitive, JavaScript temporarily wraps it in an object: + +<Steps> + <Step title="You Call a Method on a Primitive"> + ```javascript + "hello".toUpperCase() + ``` + </Step> + + <Step title="JavaScript Creates a Wrapper Object"> + Behind the scenes, JavaScript does something like: + ```javascript + (new String("hello")).toUpperCase() + ``` + </Step> + + <Step title="Method Executes and Returns"> + The `toUpperCase()` method runs and returns `"HELLO"`. + </Step> + + <Step title="Wrapper Object Is Discarded"> + The temporary `String` object is thrown away. The original primitive `"hello"` is unchanged. + </Step> +</Steps> + +### Wrapper Objects + +Each primitive type (except `null` and `undefined`) has a corresponding wrapper object: + +| Primitive | Wrapper Object | +|-----------|----------------| +| `string` | `String` | +| `number` | `Number` | +| `boolean` | `Boolean` | +| `bigint` | `BigInt` | +| `symbol` | `Symbol` | + +### Don't Use new String() etc. + +You can create wrapper objects manually, but **don't**: + +```javascript +// Don't do this! +let strObj = new String("hello"); +console.log(typeof strObj); // "object" (not "string"!) +console.log(strObj === "hello"); // false (object vs primitive) + +// Do this instead +let str = "hello"; +console.log(typeof str); // "string" +``` + +<Warning> +Using `new String()`, `new Number()`, or `new Boolean()` creates **objects**, not primitives. This can cause confusing bugs with equality checks and typeof. +</Warning> + +--- + +## null vs undefined + +These two "empty" values confuse many developers. Here's how they differ: + +<Tabs> + <Tab title="Side-by-Side Comparison"> + | Aspect | `undefined` | `null` | + |--------|-------------|--------| + | **Meaning** | "No value assigned yet" | "Intentionally empty" | + | **Set by** | JavaScript automatically | Developer explicitly | + | **typeof** | `"undefined"` | `"object"` (bug) | + | **In JSON** | Omitted from output | Preserved as `null` | + | **Default params** | Triggers default | Doesn't trigger default | + | **Loose equality** | `null == undefined` is `true` | | + | **Strict equality** | `null === undefined` is `false` | | + </Tab> + <Tab title="When JavaScript Uses undefined"> + ```javascript + // 1. Uninitialized variables + let x; + console.log(x); // undefined + + // 2. Missing function arguments + function greet(name) { + console.log(name); + } + greet(); // undefined + + // 3. No return statement + function noReturn() {} + console.log(noReturn()); // undefined + + // 4. Non-existent properties + let obj = {}; + console.log(obj.missing); // undefined + + // 5. Array holes + let arr = [1, , 3]; + console.log(arr[1]); // undefined + ``` + </Tab> + <Tab title="When to Use null"> + ```javascript + // 1. Explicitly "clearing" a value + let user = { name: "Alice" }; + user = null; // User logged out + + // 2. Function returning "no result" + function findUser(id) { + // Search logic... + return null; // Not found + } + + // 3. Optional object properties + let config = { + cache: true, + timeout: null // Explicitly no timeout + }; + + // 4. Resetting references + let timer = setTimeout(callback, 1000); + clearTimeout(timer); + timer = null; // Clear reference + ``` + </Tab> +</Tabs> + +### Best Practices + +```javascript +// Check for either null or undefined (loose equality) +if (value == null) { + console.log("Value is null or undefined"); +} + +// Check for specifically undefined +if (value === undefined) { + console.log("Value is undefined"); +} + +// Check for specifically null +if (value === null) { + console.log("Value is null"); +} + +// Check for "has a value" (not null/undefined) +if (value != null) { + console.log("Value exists"); +} +``` + +--- + +## The #1 Primitive Mistake: Using Wrapper Constructors + +The most common mistake developers make with primitives is using `new String()`, `new Number()`, or `new Boolean()` instead of literal values. + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ PRIMITIVES VS WRAPPER OBJECTS │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ WRONG WAY RIGHT WAY │ +│ ───────── ───────── │ +│ new String("hello") "hello" │ +│ new Number(42) 42 │ +│ new Boolean(true) true │ +│ │ +│ typeof new String("hi") → "object" typeof "hi" → "string" │ +│ new String("hi") === "hi" → false "hi" === "hi" → true │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +```javascript +// ❌ WRONG - Creates an object, not a primitive +const str = new String("hello"); +console.log(typeof str); // "object" (not "string"!) +console.log(str === "hello"); // false (object vs primitive) + +// ✓ CORRECT - Use primitive literals +const str2 = "hello"; +console.log(typeof str2); // "string" +console.log(str2 === "hello"); // true +``` + +<Warning> +**The Trap:** Using `new String()`, `new Number()`, or `new Boolean()` creates wrapper **objects**, not primitives. This breaks equality checks (`===`), `typeof` comparisons, and can cause subtle bugs. Always use literal syntax: `"hello"`, `42`, `true`. +</Warning> + +<Tip> +**Rule of Thumb:** Never use `new` with `String`, `Number`, or `Boolean`. The only exception is when you intentionally need the wrapper object (which is rare). For type conversion, use them as functions without `new`: `String(123)` returns `"123"` (a primitive). +</Tip> + +--- + +## JavaScript Quirks & Gotchas + +JavaScript has some famous "weird parts" that every developer should know. Most relate to primitives and type coercion. + +<AccordionGroup> + <Accordion title="1. typeof null === 'object'"> + ```javascript + console.log(typeof null); // "object" + ``` + + **Why?** This is a bug from JavaScript's first implementation in 1995. In the original code, values had a small label to identify their type. Objects had the label `000`, and `null` was represented as the NULL pointer (`0x00`), which also had `000`. + + **Why not fixed?** A proposal to fix it was rejected because too much existing code checks `typeof x === "object"` and expects `null` to pass. + + **Workaround:** + ```javascript + // Always check for null explicitly + if (value !== null && typeof value === "object") { + // It's a real object + } + ``` + </Accordion> + + <Accordion title="2. NaN !== NaN"> + ```javascript + console.log(NaN === NaN); // false! + console.log(NaN !== NaN); // true! + ``` + + NaN is so confused about its identity that it doesn't even equal itself! + + **Why?** By the IEEE 754 specification, NaN represents "Not a Number", an undefined or unrepresentable result. Since it's not a specific number, it can't equal anything, including itself. + + **How to check for NaN:** + ```javascript + // Don't do this + if (value === NaN) { } // Never true! + + // Do this instead + if (Number.isNaN(value)) { } // ES6, recommended + if (isNaN(value)) { } // Older, has quirks + ``` + + <Note> + [`isNaN()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/isNaN) converts the value first, so `isNaN("hello")` is `true`. [`Number.isNaN()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isNaN) only returns `true` for actual `NaN`. + </Note> + </Accordion> + + <Accordion title="3. 0.1 + 0.2 !== 0.3"> + ```javascript + console.log(0.1 + 0.2); // 0.30000000000000004 + console.log(0.1 + 0.2 === 0.3); // false + ``` + + **Why?** Computers store numbers in binary. Just like 1/3 can't be perfectly represented in decimal (0.333...), 0.1 can't be perfectly represented in binary. + + **Solutions:** + ```javascript + // 1. Work in integers (cents, not dollars) — RECOMMENDED + let totalCents = 10 + 20; // 30 (accurate!) + let dollars = totalCents / 100; // 0.3 + + // 2. Use Intl.NumberFormat for display + new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD' + }).format(0.30); // "$0.30" + + // 3. Compare with tolerance for equality checks + Math.abs((0.1 + 0.2) - 0.3) < Number.EPSILON; // true (Number.EPSILON is the smallest difference) + + // 4. Use toFixed() for simple rounding + (0.1 + 0.2).toFixed(2); // "0.30" + ``` + </Accordion> + + <Accordion title="4. Empty String is Falsy, But..."> + ```javascript + console.log(Boolean("")); // false (empty string is falsy) + console.log(Boolean(" ")); // true (space is truthy!) + console.log(Boolean("0")); // true (string "0" is truthy!) + console.log(Boolean(0)); // false (number 0 is falsy) + + console.log("" == false); // true (coercion) + console.log("" === false); // false (different types) + ``` + + **The lesson:** Be careful with truthy/falsy checks on strings. An empty string `""` is falsy, but a string with just whitespace `" "` is truthy. + + ```javascript + // Check for empty or whitespace-only string + if (str.trim() === "") { + console.log("String is empty or whitespace"); + } + ``` + </Accordion> + + <Accordion title="5. + Operator String Concatenation"> + ```javascript + console.log(1 + 2); // 3 (number addition) + console.log("1" + "2"); // "12" (string concatenation) + console.log(1 + "2"); // "12" (number converted to string!) + console.log("1" + 2); // "12" (number converted to string!) + console.log(1 + 2 + "3"); // "33" (left to right: 1+2=3, then 3+"3"="33") + console.log("1" + 2 + 3); // "123" (left to right: "1"+2="12", "12"+3="123") + ``` + + **Why?** The `+` operator does addition for numbers, but concatenation for strings. When mixed, JavaScript converts numbers to strings. + + **Be explicit:** + ```javascript + // Force number addition + Number("1") + Number("2"); // 3 + parseInt("1") + parseInt("2"); // 3 + + // Force string concatenation + String(1) + String(2); // "12" + `${1}${2}`; // "12" + ``` + </Accordion> +</AccordionGroup> + +<Tip> +**Want to go deeper?** Kyle Simpson's book "You Don't Know JS: Types & Grammar" is the definitive guide to understanding these quirks. It's free to read online! +</Tip> + +--- + +## Key Takeaways + +<Info> +**The key things to remember about Primitive Types:** + +1. **7 primitives**: string, number, bigint, boolean, undefined, null, symbol + +2. **Primitives are immutable** — you can't change the value itself, only create new values + +3. **Compared by value** — `"hello" === "hello"` is true because the values match + +4. **typeof works for most types** — except `typeof null` returns `"object"` (historical bug) + +5. **Autoboxing** allows primitives to use methods — JavaScript wraps them temporarily + +6. **undefined vs null** — undefined is "not assigned," null is "intentionally empty" + +7. **Be aware of gotchas** — `NaN !== NaN`, `0.1 + 0.2 !== 0.3`, falsy values + +8. **Don't use `new String()` etc.** — creates objects, not primitives + +9. **Symbols create unique identifiers** — even `Symbol("id") !== Symbol("id")` + +10. **Use `Number.isNaN()` to check for NaN** — don't use equality comparison since `NaN !== NaN` +</Info> + +--- + +## Test Your Knowledge + +<AccordionGroup> + <Accordion title="Question 1: What are the 7 primitive types in JavaScript?"> + **Answer:** + 1. `string` - text data + 2. `number` - integers and decimals + 3. `bigint` - large integers + 4. `boolean` - true/false + 5. `undefined` - no value assigned + 6. `null` - intentionally empty + 7. `symbol` - unique identifier + + Remember: Everything else is an object (arrays, functions, dates, etc.). + </Accordion> + + <Accordion title="Question 2: What does typeof null return and why?"> + **Answer:** `typeof null` returns `"object"`. + + This is a bug from JavaScript's original implementation in 1995. Values were stored with type tags, and both objects and `null` had the same `000` tag. The bug was never fixed because too much existing code depends on this behavior. + + To check for null, use `value === null` instead. + </Accordion> + + <Accordion title="Question 3: Why does 0.1 + 0.2 !== 0.3?"> + **Answer:** Because JavaScript (like all languages) uses binary floating-point (IEEE 754) to store numbers. + + Just like 1/3 can't be perfectly represented in decimal (0.333...), 0.1 can't be perfectly represented in binary. The tiny rounding errors accumulate, giving us `0.30000000000000004` instead of `0.3`. + + Solutions: Use integers (work in cents), use `toFixed()` for display, compare with tolerance, or use a decimal math library. + </Accordion> + + <Accordion title="Question 4: What's the difference between null and undefined?"> + **Answer:** + - **`undefined`**: Means "no value has been assigned." JavaScript sets this automatically for uninitialized variables, missing function arguments, and non-existent properties. + + - **`null`**: Means "intentionally empty." Developers use this explicitly to indicate "this has no value on purpose." + + Key difference: `undefined` is the *default* empty value; `null` is the *intentional* empty value. + + ```javascript + let x; // undefined (automatic) + let y = null; // null (explicit) + ``` + </Accordion> + + <Accordion title="Question 5: How can 'hello'.toUpperCase() work if primitives have no methods?"> + **Answer:** Through **autoboxing** (also called "auto-wrapping"). + + When you call a method on a primitive: + 1. JavaScript temporarily wraps it in a wrapper object (`String`, `Number`, etc.) + 2. The method is called on the wrapper object + 3. The result is returned + 4. The wrapper object is discarded + + So `"hello".toUpperCase()` becomes `(new String("hello")).toUpperCase()` behind the scenes. The original primitive `"hello"` is never changed. + </Accordion> + + <Accordion title="Question 6: Why can't you use === to check if a value is NaN?"> + **Answer:** Because `NaN` is the only value in JavaScript that is not equal to itself! + + ```javascript + console.log(NaN === NaN); // false! + ``` + + This is per the IEEE 754 floating-point specification. `NaN` represents an undefined or unrepresentable mathematical result, so it can't equal anything, including itself. + + **How to check for NaN:** + ```javascript + // ❌ WRONG - Never works! + if (value === NaN) { } + + // ✓ CORRECT - Use Number.isNaN() + if (Number.isNaN(value)) { } + ``` + </Accordion> +</AccordionGroup> + +--- + +## Related Concepts + +<CardGroup cols={2}> + <Card title="Value Types vs Reference Types" icon="clone" href="/concepts/value-reference-types"> + How primitives and objects are stored differently in memory + </Card> + <Card title="Type Coercion" icon="shuffle" href="/concepts/type-coercion"> + How JavaScript converts between types automatically + </Card> + <Card title="== vs === vs typeof" icon="equals" href="/concepts/equality-operators"> + Understanding equality operators and type checking + </Card> + <Card title="Scope & Closures" icon="layer-group" href="/concepts/scope-and-closures"> + How variables are accessed and how functions remember their environment + </Card> +</CardGroup> + +--- + +## Reference + +<CardGroup cols={2}> + <Card title="Primitive — MDN Glossary" icon="book" href="https://developer.mozilla.org/en-US/docs/Glossary/Primitive"> + Official MDN glossary definition of primitive values in JavaScript. + </Card> + <Card title="JavaScript data types and data structures — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures"> + Comprehensive MDN guide to JavaScript's type system and data structures. + </Card> + <Card title="typeof operator — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof"> + Complete reference for the typeof operator including its quirks and return values. + </Card> + <Card title="Symbol — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol"> + Deep dive into JavaScript Symbols, well-known symbols, and use cases. + </Card> +</CardGroup> + +## Articles + +<CardGroup cols={2}> + <Card title="Primitive and Non-primitive data-types in JavaScript" icon="newspaper" href="https://www.geeksforgeeks.org/primitive-and-non-primitive-data-types-in-javascript"> + Beginner-friendly overview covering all 7 primitive types with a comparison table showing the differences between primitives and objects. + </Card> + <Card title="How numbers are encoded in JavaScript" icon="newspaper" href="http://2ality.com/2012/04/number-encoding.html"> + Expert deep-dive by Dr. Axel Rauschmayer into IEEE 754 floating-point representation, explaining why 0.1 + 0.2 !== 0.3 and how JavaScript stores numbers internally. + </Card> + <Card title="(Not) Everything in JavaScript is an Object" icon="newspaper" href="https://dev.to/d4nyll/not-everything-in-javascript-is-an-object"> + Debunks the common myth that "everything in JS is an object." Shows how autoboxing creates the illusion that primitives have methods, with diagrams explaining what happens behind the scenes. + </Card> + <Card title="Methods of Primitives" icon="newspaper" href="https://javascript.info/primitives-methods"> + The javascript.info guide walks through each wrapper type (String, Number, Boolean) and includes interactive tasks to test your understanding. One of the best resources for learning autoboxing. + </Card> + <Card title="The Differences Between Object.freeze() vs Const" icon="newspaper" href="https://medium.com/@bolajiayodeji/the-differences-between-object-freeze-vs-const-in-javascript-4eacea534d7c"> + Clears up the common confusion between const (prevents reassignment) and immutability (prevents mutation). Short and beginner-friendly. + </Card> +</CardGroup> + +## Books + +<Card title="You Don't Know JS: Types & Grammar — Kyle Simpson" icon="book" href="https://github.com/getify/You-Dont-Know-JS/blob/2nd-ed/types-grammar/README.md"> + The definitive deep-dive into JavaScript types. Free to read online. Covers primitives, coercion, and the "weird parts" that trip up developers. Essential reading for understanding JavaScript's type system. +</Card> + +## Courses + +<CardGroup cols={2}> + <Card title="JavaScript: Understanding the Weird Parts (First 3.5 Hours) — Anthony Alicea" icon="graduation-cap" href="https://www.youtube.com/watch?v=Bv_5Zv5c-Ts"> + Free preview of one of the most acclaimed JavaScript courses ever made. Covers types, coercion, and the "weird parts" that confuse developers. Perfect starting point before buying the full course. + </Card> + <Card title="JavaScript: Understanding the Weird Parts (Full Course) — Anthony Alicea" icon="graduation-cap" href="https://www.udemy.com/course/understand-javascript/"> + The complete 12-hour course covering types, operators, objects, and engine internals. Anthony's explanations of scope, closures, and prototypes are particularly helpful for intermediate developers. + </Card> + <Card title="Introduction to Primitives — Piccalilli" icon="graduation-cap" href="https://piccalil.li/javascript-for-everyone/lessons/9"> + Part of the "JavaScript for Everyone" course by Mat Marquis. This module covers all 7 primitive types with dedicated lessons for Numbers, Strings, Booleans, null/undefined, BigInt, and Symbol. Beautifully written with a fun narrative style. + </Card> +</CardGroup> + +## Videos + +<CardGroup cols={2}> + <Card title="JavaScript Reference vs Primitive Types — Academind" icon="video" href="https://www.youtube.com/watch?v=9ooYYRLdg_g"> + Academind's Max shows what happens in memory when you copy primitives vs objects. The side-by-side code examples make the difference immediately obvious. + </Card> + <Card title="Value Types and Reference Types — Programming with Mosh" icon="video" href="https://www.youtube.com/watch?v=e-_mDyqm2oU"> + Mosh Hamedani's clear teaching style makes this complex topic easy to understand. Includes practical examples showing memory behavior. + </Card> + <Card title="Everything You Never Wanted to Know About JavaScript Numbers — JSConf" icon="video" href="https://www.youtube.com/watch?v=MqHDDtVYJRI"> + JSConf talk by Bartek Szopka diving deep into the quirks of JavaScript numbers. Covers IEEE 754, precision issues, and edge cases. + </Card> +</CardGroup> diff --git a/docs/concepts/promises.mdx b/docs/concepts/promises.mdx new file mode 100644 index 00000000..dc7c1fd4 --- /dev/null +++ b/docs/concepts/promises.mdx @@ -0,0 +1,1740 @@ +--- +title: "Promises: Managing Async Operations in JavaScript" +sidebarTitle: "Promises: Managing Async Operations" +description: "Learn JavaScript Promises for handling async operations. Understand how to create, chain, and combine Promises, handle errors properly, and avoid common pitfalls." +--- + +What if you could represent a value that doesn't exist yet? What if instead of deeply nested callbacks, you could write asynchronous code that reads almost like synchronous code? + +```javascript +// Instead of callback hell... +getUser(userId, function(user) { + getPosts(user.id, function(posts) { + getComments(posts[0].id, function(comments) { + console.log(comments) + }) + }) +}) + +// ...Promises give you this: +getUser(userId) + .then(user => getPosts(user.id)) + .then(posts => getComments(posts[0].id)) + .then(comments => console.log(comments)) +``` + +A **[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)** is an object representing the eventual completion or failure of an asynchronous operation. It's a placeholder for a value that will show up later. Think of it like an order ticket at a restaurant that you'll trade for food when it's ready. + +<Info> +**What you'll learn in this guide:** +- What Promises are and why they were invented +- The three states of a Promise: pending, fulfilled, rejected +- How to create Promises with the Promise constructor +- How to consume Promises with `.then()`, `.catch()`, and `.finally()` +- How Promise chaining works and why it's powerful +- All the Promise static methods: `all`, `allSettled`, `race`, `any`, `resolve`, `reject`, `withResolvers`, `try` +- Common patterns and mistakes to avoid +</Info> + +<Warning> +**Prerequisite:** This guide assumes you understand [Callbacks](/concepts/callbacks). Promises were invented to solve problems with callbacks, so understanding callbacks will help you appreciate why Promises exist and how they improve async code. +</Warning> + +--- + +## What is a Promise? + +A **[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)** is a JavaScript object that represents the eventual result of an asynchronous operation. When you create a Promise, you're saying: "I don't have the value right now, but I *promise* to give you a value (or an error) later." + +```javascript +// A Promise that resolves after 1 second +const promise = new Promise((resolve, reject) => { + setTimeout(() => { + resolve('Hello from the future!') + }, 1000) +}) + +// Consuming the Promise +promise.then(value => { + console.log(value) // "Hello from the future!" (after 1 second) +}) +``` + +Unlike callbacks that you pass *into* functions, Promises are objects you get *back* from functions. This small change unlocks useful patterns like chaining, composition, and unified error handling. + +<CardGroup cols={2}> + <Card title="Promise — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise"> + Official MDN documentation for the Promise object + </Card> + <Card title="Using Promises — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises"> + MDN guide on how to use Promises effectively + </Card> +</CardGroup> + +--- + +## The Restaurant Order Analogy + +Let's make this concrete. Imagine you're at a busy restaurant: + +1. **You place an order** — The waiter gives you an order ticket (a Promise) +2. **You wait** — The kitchen is cooking (the async operation is pending) +3. **One of two things happens:** + - **Food is ready** — You exchange your ticket for food (Promise fulfilled) + - **Kitchen ran out of ingredients** — You get an apology instead (Promise rejected) + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ THE PROMISE LIFECYCLE │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ YOU KITCHEN │ +│ ┌──────────┐ ┌──────────────┐ │ +│ │ │ "I'll have the │ │ │ +│ │ :) │ ─────pasta!─────► │ [chef] │ │ +│ │ │ │ │ │ +│ └──────────┘ └──────────────┘ │ +│ │ │ │ +│ │ Here's your │ │ +│ │ ORDER TICKET │ Cooking... │ +│ │ (Promise) │ (Pending) │ +│ ▼ │ │ +│ ┌──────────┐ │ │ +│ │ TICKET │ │ │ +│ │ #42 │◄───────────────────────────┘ │ +│ │ PENDING │ │ +│ └──────────┘ │ +│ │ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ OUTCOME │ │ +│ ├─────────────────────────┬───────────────────────────────┤ │ +│ │ │ │ │ +│ │ FULFILLED │ REJECTED │ │ +│ │ ┌──────────┐ │ ┌──────────┐ │ │ +│ │ │ PASTA │ │ │ SORRY! │ │ │ +│ │ │ :D │ │ │ No more │ │ │ +│ │ │ │ │ │ pasta │ │ │ +│ │ └──────────┘ │ └──────────┘ │ │ +│ │ You got what │ Something went │ │ +│ │ you ordered! │ wrong │ │ +│ │ │ │ │ +│ └─────────────────────────┴───────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +Here's how this maps to JavaScript: + +| Restaurant | Promise | Code | +|------------|---------|------| +| Order ticket | Promise object | `const promise = fetch(url)` | +| Waiting for food | Pending state | Promise exists but hasn't settled | +| Food arrives | Fulfilled state | `resolve(value)` was called | +| Out of ingredients | Rejected state | `reject(error)` was called | +| Picking up food | `.then()` handler | `promise.then(food => eat(food))` | +| Handling problems | `.catch()` handler | `promise.catch(err => complain(err))` | + +Here's the important part: **once your order is fulfilled or rejected, it doesn't change**. You can't un-eat the pasta or un-reject the apology. Similarly, once a Promise settles, its state is permanent. + +--- + +## Why Promises? The Callback Problem + +Before we go further, let's quickly look at why Promises were invented. If you've read the [Callbacks guide](/concepts/callbacks), you know about "callback hell": the deeply nested, hard-to-read code that happens when you chain multiple async operations: + +```javascript +// Callback Hell - The Pyramid of Doom +getUserData(userId, function(error, user) { + if (error) { + handleError(error) + return + } + getOrderHistory(user.id, function(error, orders) { + if (error) { + handleError(error) + return + } + getOrderDetails(orders[0].id, function(error, details) { + if (error) { + handleError(error) + return + } + getShippingStatus(details.shipmentId, function(error, status) { + if (error) { + handleError(error) + return + } + console.log(status) + }) + }) + }) +}) +``` + +The same logic with Promises: + +```javascript +// Promises - Flat and Readable +getUserData(userId) + .then(user => getOrderHistory(user.id)) + .then(orders => getOrderDetails(orders[0].id)) + .then(details => getShippingStatus(details.shipmentId)) + .then(status => console.log(status)) + .catch(error => handleError(error)) // One place for ALL errors! +``` + +<Tip> +**Why Promises are better:** +- **Flat structure** — No more pyramid of doom +- **Unified error handling** — One `.catch()` handles all errors in the chain +- **Composition** — Promises can be combined with `Promise.all()`, `Promise.race()`, etc. +- **Guaranteed async** — `.then()` callbacks always run asynchronously (on the microtask queue) +- **Return values** — Promises are objects you can store, pass around, and return from functions +</Tip> + +--- + +## Promise States and Fate + +Every Promise is in one of three **states**: + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ PROMISE STATES │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌───────────┐ │ +│ │ PENDING │ │ +│ │ │ │ +│ │ Waiting │ │ +│ │ for │ │ +│ │ result │ │ +│ └─────┬─────┘ │ +│ │ │ +│ ┌─────────────────┴─────────────────┐ │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌───────────────┐ ┌───────────────┐ │ +│ │ FULFILLED │ │ REJECTED │ │ +│ │ │ │ │ │ +│ │ Success! │ │ Failed! │ │ +│ │ Has value │ │ Has reason │ │ +│ └───────────────┘ └───────────────┘ │ +│ │ +│ ◄─────────────── SETTLED (final state) ───────────────► │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +| State | Description | Can Change? | +|-------|-------------|-------------| +| **Pending** | Initial state. The async operation is still in progress. | Yes | +| **Fulfilled** | The operation completed successfully. The Promise has a value. | No | +| **Rejected** | The operation failed. The Promise has a reason (error). | No | + +A Promise that is either fulfilled or rejected is called **settled**. Once settled, a Promise's state is locked in and never changes. + +```javascript +const promise = new Promise((resolve, reject) => { + resolve('first') // Promise is now FULFILLED with value 'first' + resolve('second') // Ignored! Promise already settled + reject('error') // Also ignored! Promise already settled +}) + +promise.then(value => { + console.log(value) // "first" +}) +``` + +<Warning> +**Important:** Calling `resolve()` or `reject()` multiple times does nothing after the first call. The Promise settles once and only once. +</Warning> + +### Promise Fate: Resolved vs Unresolved + +There's a subtle but useful distinction between a Promise's **state** and its **fate**: + +- **State** = pending, fulfilled, or rejected +- **Fate** = resolved or unresolved + +Think of it like this: when you place your restaurant order, your fate is "sealed" the moment the waiter writes it down, even though you haven't received your food yet (still pending). You can't change your order anymore. + +A Promise is **resolved** when its fate is sealed, either because it's already settled, or because it's "locked in" to follow another Promise: + +```javascript +const innerPromise = new Promise(resolve => { + setTimeout(() => resolve('inner value'), 1000) +}) + +const outerPromise = new Promise(resolve => { + resolve(innerPromise) // Resolving with another Promise! +}) + +// outerPromise is now "resolved" (its fate is locked to innerPromise) +// but it's still "pending" (its state hasn't settled yet) + +outerPromise.then(value => { + console.log(value) // "inner value" (after 1 second) +}) +``` + +When you resolve a Promise with another Promise, the outer Promise "adopts" the state of the inner one. This is called **Promise unwrapping**. The outer Promise automatically follows whatever happens to the inner Promise. + +### Thenables + +JavaScript doesn't just work with native Promises — it also supports **thenables**. A thenable is any object with a `.then()` method. This allows Promises to interoperate with Promise-like objects from libraries: + +```javascript +// A thenable is any object with a .then() method +const thenable = { + then(onFulfilled, onRejected) { + onFulfilled(42) + } +} + +// Promise.resolve() unwraps thenables +Promise.resolve(thenable).then(value => { + console.log(value) // 42 +}) + +// Returning a thenable from .then() also works +Promise.resolve('start') + .then(() => thenable) + .then(value => console.log(value)) // 42 +``` + +This is why `Promise.resolve()` doesn't always return a new Promise — if you pass it a native Promise, it returns the same Promise: + +```javascript +const p = Promise.resolve('hello') +const p2 = Promise.resolve(p) +console.log(p === p2) // true +``` + +--- + +## Creating Promises + +### The Promise Constructor + +You create a new Promise using the `Promise` constructor, which takes an **executor function**: + +```javascript +const promise = new Promise((resolve, reject) => { + // Your async code here + // Call resolve(value) on success + // Call reject(error) on failure +}) +``` + +The executor receives two arguments: +- **`resolve(value)`** — Call this to fulfill the Promise with a value +- **`reject(reason)`** — Call this to reject the Promise with an error + +<Warning> +**Heads up:** The executor function runs **immediately and synchronously** when you create the Promise. Only the `.then()` callbacks are asynchronous. + +```javascript +console.log('Before Promise') + +const promise = new Promise((resolve, reject) => { + console.log('Inside executor (synchronous!)') + resolve('done') +}) + +console.log('After Promise') + +promise.then(value => { + console.log('Inside then (asynchronous)') +}) + +console.log('After then') + +// Output: +// Before Promise +// Inside executor (synchronous!) +// After Promise +// After then +// Inside then (asynchronous) +``` +</Warning> + +### Wrapping setTimeout in a Promise + +You'll often use the Promise constructor to wrap old callback-style code. Let's create a handy `delay` function: + +```javascript +// Create a Promise that resolves after ms milliseconds +function delay(ms) { + return new Promise(resolve => { + setTimeout(resolve, ms) + }) +} + +// Usage +console.log('Starting...') + +delay(2000).then(() => { + console.log('2 seconds have passed!') +}) + +// Or with a value +function delayedValue(value, ms) { + return new Promise(resolve => { + setTimeout(() => resolve(value), ms) + }) +} + +delayedValue('Hello!', 1000).then(message => { + console.log(message) // "Hello!" (after 1 second) +}) +``` + +### Wrapping Callback-Based APIs + +Here's a real-world example: turning a callback-based image loader into a Promise: + +```javascript +// Original callback-based function +function loadImageCallback(url, onSuccess, onError) { + const img = new Image() + img.onload = () => onSuccess(img) + img.onerror = () => onError(new Error(`Failed to load ${url}`)) + img.src = url +} + +// Promise-based wrapper +function loadImage(url) { + return new Promise((resolve, reject) => { + const img = new Image() + img.onload = () => resolve(img) + img.onerror = () => reject(new Error(`Failed to load ${url}`)) + img.src = url + }) +} + +// Now you can use it with .then() or async/await! +loadImage('https://example.com/photo.jpg') + .then(img => { + console.log(`Loaded image: ${img.width}x${img.height}`) + document.body.appendChild(img) + }) + .catch(error => { + console.error('Failed to load image:', error.message) + }) +``` + +### Handling Errors in the Executor + +If an error is thrown inside the executor, the Promise is automatically rejected: + +```javascript +const promise = new Promise((resolve, reject) => { + throw new Error('Something went wrong!') + // No need to call reject() — the throw does it automatically +}) + +promise.catch(error => { + console.log(error.message) // "Something went wrong!" +}) +``` + +This is equivalent to: + +```javascript +const promise = new Promise((resolve, reject) => { + reject(new Error('Something went wrong!')) +}) +``` + +--- + +## Consuming Promises: then, catch, finally + +Once you have a Promise, you need to actually *do* something with it when it finishes. JavaScript gives you three methods for this. + +### .then() — The Core Method + +The **[`.then()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then)** method is the primary way to handle Promise results. It takes up to two callbacks: + +```javascript +promise.then(onFulfilled, onRejected) +``` + +- **`onFulfilled(value)`** — Called when the Promise is fulfilled +- **`onRejected(reason)`** — Called when the Promise is rejected + +```javascript +const promise = new Promise((resolve, reject) => { + const random = Math.random() + if (random > 0.5) { + resolve(`Success! Random was ${random}`) + } else { + reject(new Error(`Failed! Random was ${random}`)) + } +}) + +promise.then( + value => console.log('Fulfilled:', value), + error => console.log('Rejected:', error.message) +) +``` + +Most commonly, you'll only pass the first callback and use `.catch()` for errors: + +```javascript +promise.then(value => { + console.log('Got value:', value) +}) +``` + +### .catch() — Handling Rejections + +The **[`.catch()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch)** method is syntactic sugar for `.then(undefined, onRejected)`: + +```javascript +// These are equivalent: +promise.catch(error => handleError(error)) +promise.then(undefined, error => handleError(error)) +``` + +Using `.catch()` is cleaner and more readable: + +```javascript +fetchUserData(userId) + .then(user => processUser(user)) + .then(result => saveResult(result)) + .catch(error => { + // Catches errors from fetchUserData, processUser, OR saveResult + console.error('Something went wrong:', error.message) + }) +``` + +### .finally() — Cleanup Code + +The **[`.finally()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/finally)** method runs code no matter if the Promise was fulfilled or rejected. It's great for cleanup: + +```javascript +let isLoading = true + +fetchData(url) + .then(data => { + displayData(data) + }) + .catch(error => { + displayError(error) + }) + .finally(() => { + // This runs no matter what! + isLoading = false + hideLoadingSpinner() + }) +``` + +<Note> +**How `.finally()` works:** +- It receives no arguments (it doesn't know if the Promise fulfilled or rejected) +- It returns a Promise that "passes through" the original value/error +- If you throw or return a rejected Promise in `.finally()`, that error propagates +</Note> + +```javascript +Promise.resolve('hello') + .finally(() => { + console.log('Cleanup!') + // Return value is ignored + return 'ignored' + }) + .then(value => { + console.log(value) // "hello" (not "ignored"!) + }) +``` + +### Every Handler Returns a New Promise + +This is **key** to understand: `.then()`, `.catch()`, and `.finally()` all return **new Promises**. This is what makes chaining possible: + +```javascript +const promise1 = Promise.resolve(1) +const promise2 = promise1.then(x => x + 1) +const promise3 = promise2.then(x => x + 1) + +// promise1, promise2, and promise3 are THREE DIFFERENT Promises! + +console.log(promise1 === promise2) // false +console.log(promise2 === promise3) // false + +promise3.then(value => console.log(value)) // 3 +``` + +--- + +## Promise Chaining + +Promise chaining is where Promises shine. Since each `.then()` returns a new Promise, you can chain them together: + +```javascript +Promise.resolve(1) + .then(x => { + console.log(x) // 1 + return x + 1 + }) + .then(x => { + console.log(x) // 2 + return x + 1 + }) + .then(x => { + console.log(x) // 3 + return x + 1 + }) + .then(x => { + console.log(x) // 4 + }) +``` + +### How Chaining Works + +The value returned from a `.then()` callback becomes the fulfillment value of the Promise returned by `.then()`: + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ PROMISE CHAINING FLOW │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Promise.resolve(1) │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────┐ │ +│ │ .then(x => x * 2) │ │ +│ │ │ │ +│ │ Input: 1 │ │ +│ │ Return: 2 │ │ +│ │ Output Promise: fulfilled with 2 │ │ +│ └─────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────┐ │ +│ │ .then(x => x + 10) │ │ +│ │ │ │ +│ │ Input: 2 │ │ +│ │ Return: 12 │ │ +│ │ Output Promise: fulfilled with 12 │ │ +│ └─────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────┐ │ +│ │ .then(x => console.log(x)) │ │ +│ │ │ │ +│ │ Input: 12 │ │ +│ │ Console: "12" │ │ +│ │ Return: undefined │ │ +│ │ Output Promise: fulfilled with undefined │ │ +│ └─────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### Returning Promises in Chains + +If you return a Promise from a `.then()` callback, the chain waits for it to finish: + +```javascript +function fetchUser(id) { + return new Promise(resolve => { + setTimeout(() => resolve({ id, name: 'Alice' }), 100) + }) +} + +function fetchPosts(userId) { + return new Promise(resolve => { + setTimeout(() => resolve([ + { id: 1, title: 'First Post' }, + { id: 2, title: 'Second Post' } + ]), 100) + }) +} + +// Chain of async operations +fetchUser(1) + .then(user => { + console.log('Got user:', user.name) + return fetchPosts(user.id) // Return a Promise + }) + .then(posts => { + // This waits for fetchPosts to complete! + console.log('Got posts:', posts.length) + }) + +// Output: +// Got user: Alice +// Got posts: 2 +``` + +<Tip> +**The #1 Rule of Chaining:** Always `return` from your `.then()` callbacks! Forgetting to return is the most common Promise mistake. + +```javascript +// ❌ WRONG - forgot to return +fetchUser(1) + .then(user => { + fetchPosts(user.id) // Oops! Not returned + }) + .then(posts => { + console.log(posts) // undefined! The Promise wasn't returned + }) + +// ✓ CORRECT - return the Promise +fetchUser(1) + .then(user => { + return fetchPosts(user.id) // Explicitly return + }) + .then(posts => { + console.log(posts) // [{ id: 1, ... }, { id: 2, ... }] + }) + +// ✓ ALSO CORRECT - arrow function implicit return +fetchUser(1) + .then(user => fetchPosts(user.id)) // Implicit return + .then(posts => console.log(posts)) +``` +</Tip> + +### Transforming Values Through the Chain + +Each step in the chain can transform the value: + +```javascript +Promise.resolve('hello') + .then(str => str.toUpperCase()) // 'HELLO' + .then(str => str + '!') // 'HELLO!' + .then(str => str.repeat(3)) // 'HELLO!HELLO!HELLO!' + .then(str => str.split('!')) // ['HELLO', 'HELLO', 'HELLO', ''] + .then(arr => arr.filter(s => s.length)) // ['HELLO', 'HELLO', 'HELLO'] + .then(arr => arr.length) // 3 + .then(count => console.log(count)) // Logs: 3 +``` + +--- + +## Error Handling + +Error handling is where Promises shine. Errors automatically flow down the chain until something catches them. + +### Error Propagation + +When a Promise is rejected or an error is thrown, it "skips" all `.then()` callbacks until it finds a `.catch()`: + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ ERROR PROPAGATION │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Promise.reject(new Error('Oops!')) │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────┐ │ +│ │ .then(x => x * 2) │ ◄── SKIPPED │ +│ └─────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────┐ │ +│ │ .then(x => x + 10) │ ◄── SKIPPED │ +│ └─────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────┐ │ +│ │ .catch(err => console.log(err.message)) │ ◄── CAUGHT HERE! │ +│ │ │ │ +│ │ Output: "Oops!" │ │ +│ └─────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────┐ │ +│ │ .then(() => console.log('Recovered!')) │ ◄── RUNS (chain │ +│ └─────────────────────────────────────────────┘ continues) │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +```javascript +Promise.reject(new Error('Oops!')) + .then(x => { + console.log('This never runs') + return x * 2 + }) + .then(x => { + console.log('This never runs either') + return x + 10 + }) + .catch(error => { + console.log('Caught:', error.message) // "Caught: Oops!" + return 'recovered' + }) + .then(value => { + console.log('Continued with:', value) // "Continued with: recovered" + }) +``` + +### Throwing Errors in .then() + +If you throw an error in a `.then()` callback (or return a rejected Promise), the chain rejects: + +```javascript +Promise.resolve('start') + .then(value => { + console.log(value) // "start" + throw new Error('Something went wrong!') + }) + .then(value => { + console.log('This is skipped') + }) + .catch(error => { + console.log('Caught:', error.message) // "Caught: Something went wrong!" + }) +``` + +### Re-throwing Errors + +Sometimes you want to log an error but still let it bubble up: + +```javascript +fetchData(url) + .catch(error => { + // Log the error + console.error('Error fetching data:', error.message) + + // Re-throw to continue propagating + throw error + }) + .then(data => { + // This won't run if there was an error + processData(data) + }) + .catch(error => { + // Handle at a higher level + showUserError('Failed to load data') + }) +``` + +### Multiple .catch() Handlers + +You can have multiple `.catch()` handlers in a chain for different error handling strategies: + +```javascript +fetchUser(userId) + .then(user => { + if (!user.isActive) { + throw new Error('User is inactive') + } + return fetchUserPosts(user.id) + }) + .catch(error => { + // Handle user-related errors + if (error.message === 'User is inactive') { + return [] // Return empty posts for inactive users + } + throw error // Re-throw other errors + }) + .then(posts => renderPosts(posts)) + .catch(error => { + // Handle all other errors (network, rendering, etc.) + console.error('Failed:', error) + showFallbackUI() + }) +``` + +### The Unhandled Rejection Problem + +<Warning> +**Always handle Promise rejections!** If a Promise is rejected and there's no `.catch()` handler, modern JavaScript environments will warn you about an "unhandled promise rejection": + +```javascript +// ❌ BAD - Unhandled rejection +Promise.reject(new Error('Oops!')) + +// ❌ BAD - Error in .then() with no .catch() +Promise.resolve('data') + .then(data => { + throw new Error('Processing failed!') + }) +// UnhandledPromiseRejection warning! + +// ✓ GOOD - Always have a .catch() +Promise.reject(new Error('Oops!')) + .catch(error => console.error('Handled:', error.message)) +``` + +In Node.js, unhandled rejections can crash your application in future versions. In browsers, they're logged as errors. +</Warning> + +--- + +## Promise Static Methods + +The `Promise` class has several static methods for creating and combining Promises. These are super useful in practice. + +### Promise.resolve() and Promise.reject() + +The simplest static methods. They create already-settled Promises: + +```javascript +// Create a fulfilled Promise +const fulfilled = Promise.resolve('success') +fulfilled.then(value => console.log(value)) // "success" + +// Create a rejected Promise +const rejected = Promise.reject(new Error('failure')) +rejected.catch(error => console.log(error.message)) // "failure" +``` + +**When are these useful?** +- Converting a regular value to a Promise for consistency +- Starting a Promise chain +- Testing Promise-based code + +```javascript +// Useful for normalizing values to Promises +function fetchData(cached) { + if (cached) { + return Promise.resolve(cached) // Return cached data as Promise + } + return fetch('/api/data').then(r => r.json()) // Fetch fresh data +} + +// Both code paths return Promises, so callers can use .then() consistently +fetchData(cachedData).then(data => render(data)) +``` + +### Promise.all() — Wait for All + +**[`Promise.all()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all)** takes an iterable of Promises and returns a single Promise that: +- **Fulfills** when ALL input Promises fulfill (with an array of values) +- **Rejects** when ANY input Promise rejects (with that error, immediately) + +```javascript +const promise1 = Promise.resolve(1) +const promise2 = Promise.resolve(2) +const promise3 = Promise.resolve(3) + +Promise.all([promise1, promise2, promise3]) + .then(values => { + console.log(values) // [1, 2, 3] + }) +``` + +**Real example: loading a dashboard** + +```javascript +async function loadDashboard(userId) { + // All three requests start simultaneously! + const [user, posts, notifications] = await Promise.all([ + fetchUser(userId), + fetchPosts(userId), + fetchNotifications(userId) + ]) + + return { user, posts, notifications } +} +``` + +**The short-circuit behavior:** + +```javascript +Promise.all([ + Promise.resolve('A'), + Promise.reject(new Error('B failed!')), // This rejects! + Promise.resolve('C') +]) + .then(values => { + console.log('Success:', values) // Never runs + }) + .catch(error => { + console.log('Failed:', error.message) // "Failed: B failed!" + // We don't get 'A' or 'C' — the whole thing fails + }) +``` + +<Tip> +**Use `Promise.all()` when:** +- You need ALL results to proceed +- Any single failure should abort the whole operation +- You want to run Promises in parallel and wait for all + +**Note:** `Promise.all([])` with an empty array resolves immediately with `[]`. Also, non-Promise values in the array are automatically wrapped with `Promise.resolve()`. +</Tip> + +### Promise.allSettled() — Wait for All (No Short-Circuit) + +**[`Promise.allSettled()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled)** waits for ALL Promises to settle, regardless of whether they fulfill or reject. It never rejects: + +```javascript +Promise.allSettled([ + Promise.resolve('A'), + Promise.reject(new Error('B failed!')), + Promise.resolve('C') +]) + .then(results => { + console.log(results) + // [ + // { status: 'fulfilled', value: 'A' }, + // { status: 'rejected', reason: Error: B failed! }, + // { status: 'fulfilled', value: 'C' } + // ] + }) +``` + +**Real example: sending notifications to multiple users** + +```javascript +async function sendNotificationsToAll(userIds, message) { + const results = await Promise.allSettled( + userIds.map(id => sendNotification(id, message)) + ) + + const succeeded = results.filter(r => r.status === 'fulfilled') + const failed = results.filter(r => r.status === 'rejected') + + console.log(`Sent: ${succeeded.length}, Failed: ${failed.length}`) + + // Log failures for debugging + failed.forEach(f => console.error('Failed:', f.reason)) + + return { succeeded: succeeded.length, failed: failed.length } +} +``` + +<Tip> +**Use `Promise.allSettled()` when:** +- You want to attempt ALL operations regardless of individual failures +- You need to know which succeeded and which failed +- Partial success is acceptable +</Tip> + +### Promise.race() — First to Settle Wins + +**[`Promise.race()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race)** returns a Promise that settles as soon as ANY input Promise settles (fulfilled or rejected): + +```javascript +const slow = new Promise(resolve => setTimeout(() => resolve('slow'), 200)) +const fast = new Promise(resolve => setTimeout(() => resolve('fast'), 100)) + +Promise.race([slow, fast]) + .then(winner => console.log(winner)) // "fast" +``` + +**Real example: adding a timeout** + +```javascript +function fetchWithTimeout(url, timeout = 5000) { + const fetchPromise = fetch(url) + + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => { + reject(new Error(`Request timed out after ${timeout}ms`)) + }, timeout) + }) + + return Promise.race([fetchPromise, timeoutPromise]) +} + +// Usage +fetchWithTimeout('https://api.example.com/data', 3000) + .then(response => response.json()) + .catch(error => { + console.error(error.message) // "Request timed out after 3000ms" + }) +``` + +<Warning> +**Watch out:** `Promise.race()` settles on the first Promise to settle, whether it fulfills OR rejects. If the fastest Promise rejects, the race rejects: + +```javascript +Promise.race([ + new Promise((_, reject) => setTimeout(() => reject(new Error('Fast failure')), 50)), + new Promise(resolve => setTimeout(() => resolve('Slow success'), 100)) +]) + .catch(error => console.log(error.message)) // "Fast failure" +``` + +**Edge case:** `Promise.race([])` with an empty array returns a Promise that **never settles** (stays pending forever). This is rarely useful and usually indicates a bug. +</Warning> + +### Promise.any() — First to Fulfill Wins + +**[`Promise.any()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/any)** returns a Promise that fulfills as soon as ANY input Promise fulfills. It ignores rejections unless ALL Promises reject: + +```javascript +Promise.any([ + Promise.reject(new Error('Error 1')), + Promise.resolve('Success!'), + Promise.reject(new Error('Error 2')) +]) + .then(value => console.log(value)) // "Success!" +``` + +**If ALL Promises reject, you get an AggregateError:** + +```javascript +Promise.any([ + Promise.reject(new Error('Error 1')), + Promise.reject(new Error('Error 2')), + Promise.reject(new Error('Error 3')) +]) + .catch(error => { + console.log(error.name) // "AggregateError" + console.log(error.errors) // [Error: Error 1, Error: Error 2, Error: Error 3] + }) +``` + +**Real example: trying multiple CDN mirrors** + +```javascript +async function fetchFromFastestMirror(mirrors) { + try { + // Returns data from whichever mirror responds first + const data = await Promise.any( + mirrors.map(mirror => fetch(mirror).then(r => r.json())) + ) + return data + } catch (error) { + // All mirrors failed + throw new Error('All mirrors failed: ' + error.errors.map(e => e.message).join(', ')) + } +} + +const mirrors = [ + 'https://mirror1.example.com/data', + 'https://mirror2.example.com/data', + 'https://mirror3.example.com/data' +] + +fetchFromFastestMirror(mirrors) + .then(data => console.log('Got data:', data)) + .catch(error => console.error(error.message)) +``` + +<Tip> +**Use `Promise.any()` when:** +- You only need one successful result +- You have multiple sources/fallbacks and want the first success +- Rejections should be ignored unless everything fails + +**Edge case:** `Promise.any([])` with an empty array immediately rejects with an `AggregateError` (since there are no Promises that could fulfill). +</Tip> + +### Comparison Table + +| Method | Fulfills when... | Rejects when... | Empty array `[]` | Use case | +|--------|-----------------|-----------------|------------------|----------| +| `Promise.all()` | ALL fulfill | ANY rejects | Fulfills with `[]` | Need all results, fail-fast | +| `Promise.allSettled()` | ALL settle | Never | Fulfills with `[]` | Need all results, tolerate failures | +| `Promise.race()` | First to settle fulfills | First to settle rejects | Never settles | Timeout, fastest response | +| `Promise.any()` | ANY fulfills | ALL reject | Rejects (AggregateError) | First success, ignore failures | + +### Promise.withResolvers() + +**[`Promise.withResolvers()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers)** (ES2024) returns an object containing a new Promise and the functions to resolve/reject it. This is useful when you need to resolve a Promise from outside its executor: + +```javascript +const { promise, resolve, reject } = Promise.withResolvers() + +// Resolve it later from anywhere +setTimeout(() => resolve('Done!'), 1000) + +promise.then(value => console.log(value)) // "Done!" (after 1 second) +``` + +**Before `withResolvers()`, you had to do this:** + +```javascript +let resolve, reject +const promise = new Promise((res, rej) => { + resolve = res + reject = rej +}) + +// Now resolve/reject are available outside +``` + +### Promise.try() + +**[`Promise.try()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/try)** (Baseline 2025) takes a callback of any kind and wraps its result in a Promise. This is useful when you have a function that might be synchronous or asynchronous and you want to handle both cases uniformly: + +```javascript +// The problem: func() might throw synchronously OR return a Promise +// This doesn't catch synchronous errors: +Promise.resolve(func()).catch(handleError) // Sync throw escapes! + +// This works but is verbose: +new Promise((resolve) => resolve(func())) + +// Promise.try() is cleaner: +Promise.try(func) +``` + +**Real example: handling callbacks that might be sync or async** + +```javascript +function processData(callback) { + return Promise.try(callback) + .then(result => console.log('Result:', result)) + .catch(error => console.error('Error:', error)) + .finally(() => console.log('Done')) +} + +// Works with sync functions +processData(() => 'sync result') + +// Works with async functions +processData(async () => 'async result') + +// Catches sync throws +processData(() => { throw new Error('sync error') }) + +// Catches async rejections +processData(async () => { throw new Error('async error') }) +``` + +You can also pass arguments to the callback: + +```javascript +// Instead of creating a closure: +Promise.try(() => fetchUser(userId)) + +// You can pass arguments directly: +Promise.try(fetchUser, userId) +``` + +<Note> +`Promise.try()` calls the function **synchronously** (like the Promise constructor executor), unlike `.then()` which always runs callbacks asynchronously. If possible, it resolves the promise immediately. +</Note> + +--- + +## Common Patterns + +### Sequential Execution + +When you need to run things one at a time (not in parallel). Use this when each step depends on the previous result, like database transactions or when processing order matters (uploading files in a specific sequence). + +```javascript +// Process items one at a time +async function processSequentially(items) { + const results = [] + + for (const item of items) { + const result = await processItem(item) // Wait for each + results.push(result) + } + + return results +} + +// Or with reduce (pure Promises, no async/await): +function processSequentiallyWithReduce(items) { + return items.reduce((chain, item) => { + return chain.then(results => { + return processItem(item).then(result => { + return [...results, result] + }) + }) + }, Promise.resolve([])) +} +``` + +### Parallel Execution + +When operations don't depend on each other. Great for independent fetches like loading a dashboard where you need user data, notifications, and settings all at once. Much faster than doing them one by one. + +```javascript +// Process all items in parallel +async function processInParallel(items) { + const promises = items.map(item => processItem(item)) + return Promise.all(promises) +} + +// Example: Fetch multiple URLs at once +try { + const urls = ['/api/users', '/api/posts', '/api/comments'] + const responses = await Promise.all(urls.map(url => fetch(url))) + const data = await Promise.all(responses.map(r => r.json())) +} catch (error) { + console.error('One of the requests failed:', error) +} +``` + +### Parallel with Limit (Batching) + +When you want parallelism but don't want to hammer a server with 100 requests at once. Essential for API rate limits (e.g., "max 10 requests/second") or when processing large datasets without exhausting memory or connections. + +```javascript +async function processInBatches(items, batchSize = 3) { + const results = [] + + for (let i = 0; i < items.length; i += batchSize) { + const batch = items.slice(i, i + batchSize) + const batchResults = await Promise.all( + batch.map(item => processItem(item)) + ) + results.push(...batchResults) + } + + return results +} + +// Process 10 items, 3 at a time +const items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +const results = await processInBatches(items, 3) +// Batch 1: [1, 2, 3] (parallel) +// Batch 2: [4, 5, 6] (parallel, after batch 1) +// Batch 3: [7, 8, 9] (parallel, after batch 2) +// Batch 4: [10] (after batch 3) +``` + +### Retry Pattern + +Automatically retry when things fail. Perfect for flaky network connections, unreliable third-party APIs, or temporary server issues. For production, consider adding exponential backoff (doubling the delay each attempt). + +```javascript +async function retry(fn, maxAttempts = 3, delay = 1000) { + let lastError + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + return await fn() + } catch (error) { + lastError = error + console.log(`Attempt ${attempt} failed: ${error.message}`) + + if (attempt < maxAttempts) { + await new Promise(resolve => setTimeout(resolve, delay)) + } + } + } + + throw lastError +} + +// Usage +const data = await retry( + () => fetch('/api/flaky-endpoint').then(r => r.json()), + 3, // max attempts + 1000 // delay between attempts +) +``` + +### Converting Callbacks to Promises (Promisification) + +A helper to convert old callback-style functions to Promises. Useful when working with older Node.js APIs or third-party libraries that still use callbacks but you want clean async/await syntax. + +```javascript +function promisify(fn) { + return function(...args) { + return new Promise((resolve, reject) => { + fn(...args, (error, result) => { + if (error) { + reject(error) + } else { + resolve(result) + } + }) + }) + } +} + +// Usage example (Node.js - fs uses callbacks) +const readFile = promisify(fs.readFile) +const data = await readFile('file.txt', 'utf8') +``` + +<Note> +Node.js has this built-in: `const { promisify } = require('util')` +</Note> + +--- + +## Common Mistakes + +### Mistake 1: Forgetting to Return + +The #1 Promise mistake is forgetting to return from `.then()`: + +```javascript +// ❌ WRONG - Promise not returned, chain breaks +fetchUser(1) + .then(user => { + fetchPosts(user.id) // This Promise floats away! + }) + .then(posts => { + console.log(posts) // undefined! + }) + +// ✓ CORRECT - Return the Promise +fetchUser(1) + .then(user => { + return fetchPosts(user.id) + }) + .then(posts => { + console.log(posts) // Array of posts + }) + +// ✓ EVEN BETTER - Arrow function implicit return +fetchUser(1) + .then(user => fetchPosts(user.id)) + .then(posts => console.log(posts)) +``` + +### Mistake 2: Nesting Instead of Chaining + +Don't accidentally recreate callback hell with Promises: + +```javascript +// ❌ WRONG - Promise hell (nesting) +fetchUser(1).then(user => { + fetchPosts(user.id).then(posts => { + fetchComments(posts[0].id).then(comments => { + console.log(comments) + }) + }) +}) + +// ✓ CORRECT - Flat chain +fetchUser(1) + .then(user => fetchPosts(user.id)) + .then(posts => fetchComments(posts[0].id)) + .then(comments => console.log(comments)) +``` + +### Mistake 3: The Promise Constructor Anti-Pattern + +Don't wrap existing Promises in `new Promise()`: + +```javascript +// ❌ WRONG - Unnecessary Promise wrapper +function getUser(id) { + return new Promise((resolve, reject) => { + fetch(`/api/users/${id}`) + .then(response => response.json()) + .then(user => resolve(user)) + .catch(error => reject(error)) + }) +} + +// ✓ CORRECT - Just return the Promise! +function getUser(id) { + return fetch(`/api/users/${id}`) + .then(response => response.json()) +} +``` + +<Warning> +**The Promise constructor anti-pattern** is when you wrap something that's already a Promise. You're just adding complexity for no reason. Only use `new Promise()` when you're wrapping callback-based APIs. +</Warning> + +### Mistake 4: Forgetting Error Handling + +```javascript +// ❌ WRONG - No error handling +fetchData() + .then(data => processData(data)) + .then(result => saveResult(result)) +// If anything fails, you get an unhandled rejection! + +// ✓ CORRECT - Always have a .catch() +fetchData() + .then(data => processData(data)) + .then(result => saveResult(result)) + .catch(error => { + console.error('Operation failed:', error) + // Handle the error appropriately + }) +``` + +### Mistake 5: Using forEach with Async Operations + +```javascript +// ❌ WRONG - forEach doesn't wait for Promises +async function processAll(items) { + items.forEach(async item => { + await processItem(item) // These run in parallel, not sequentially! + }) + console.log('Done!') // Logs immediately, before processing completes +} + +// ✓ CORRECT - Use for...of for sequential +async function processAllSequential(items) { + for (const item of items) { + await processItem(item) + } + console.log('Done!') // Logs after all items processed +} + +// ✓ CORRECT - Use Promise.all for parallel +async function processAllParallel(items) { + await Promise.all(items.map(item => processItem(item))) + console.log('Done!') // Logs after all items processed +} +``` + +### Mistake 6: Microtask Timing Gotcha + +```javascript +console.log('1') + +Promise.resolve().then(() => console.log('2')) + +console.log('3') + +// Output: 1, 3, 2 (NOT 1, 2, 3!) +``` + +Promise callbacks are scheduled as **microtasks**, which run after the current synchronous code but before the next macrotask. See the [Event Loop guide](/concepts/event-loop) for details. + +--- + +## Key Takeaways + +<Info> +**The key things to remember:** + +1. **A Promise is a placeholder** — It represents a value that will show up later (or an error if something goes wrong). + +2. **Three states, one transition** — Promises go from `pending` to either `fulfilled` or `rejected`, and never change after that. + +3. **`.then()` returns a NEW Promise** — This is what enables chaining. The value you return becomes the next Promise's value. + +4. **Always return from `.then()`** — Forgetting to return is the #1 Promise mistake. Use arrow functions for implicit returns. + +5. **Errors propagate down the chain** — A rejection skips all `.then()` handlers until it hits a `.catch()`. + +6. **Always handle rejections** — Use `.catch()` at the end of chains. Unhandled rejections are bugs. + +7. **`Promise.all()` for parallel + fail-fast** — Runs Promises in parallel, fails immediately if any rejects. + +8. **`Promise.allSettled()` for partial success** — Waits for all to settle, gives you results for each. + +9. **`Promise.race()` for timeouts** — First to settle wins (fulfill OR reject). + +10. **`Promise.any()` for first success** — First to fulfill wins, ignores rejections unless all fail. +</Info> + +--- + +## Test Your Knowledge + +<AccordionGroup> + <Accordion title="Question 1: What are the three states of a Promise?"> + **Answer:** + + 1. **Pending** — Initial state, the async operation is still in progress + 2. **Fulfilled** — The operation completed successfully, the Promise has a value + 3. **Rejected** — The operation failed, the Promise has a reason (error) + + Once a Promise is fulfilled or rejected (we call this "settled"), its state is locked in forever. + </Accordion> + + <Accordion title="Question 2: What does .then() return?"> + **Answer:** + + `.then()` always returns a **new Promise**. The value returned from the `.then()` callback becomes the fulfillment value of this new Promise. + + ```javascript + const p1 = Promise.resolve(1) + const p2 = p1.then(x => x + 1) + + console.log(p1 === p2) // false - different Promises! + + p2.then(x => console.log(x)) // 2 + ``` + + If you return a Promise from the callback, the new Promise "adopts" its state. + </Accordion> + + <Accordion title="Question 3: What's the difference between Promise.all() and Promise.allSettled()?"> + **Answer:** + + | `Promise.all()` | `Promise.allSettled()` | + |-----------------|------------------------| + | Rejects immediately if ANY Promise rejects | Never rejects, waits for ALL to settle | + | Returns array of values on success | Returns array of `{status, value/reason}` objects | + | Use when all must succeed | Use when you want results regardless of failures | + + ```javascript + // Promise.all - fails fast + Promise.all([Promise.resolve(1), Promise.reject('error')]) + .catch(e => console.log(e)) // "error" + + // Promise.allSettled - gets all results + Promise.allSettled([Promise.resolve(1), Promise.reject('error')]) + .then(results => console.log(results)) + // [{status:'fulfilled',value:1}, {status:'rejected',reason:'error'}] + ``` + </Accordion> + + <Accordion title="Question 4: What happens if you resolve a Promise with another Promise?"> + **Answer:** + + The outer Promise "adopts" the state of the inner Promise. This is called Promise unwrapping or assimilation: + + ```javascript + const inner = new Promise(resolve => { + setTimeout(() => resolve('inner value'), 1000) + }) + + const outer = Promise.resolve(inner) + + // outer is now "locked in" to follow inner + // It won't fulfill until inner fulfills + + outer.then(value => console.log(value)) // "inner value" (after 1 second) + ``` + + This happens automatically. You can't have a Promise that fulfills with another Promise as its value. + </Accordion> + + <Accordion title="Question 5: What's wrong with this code?"> + ```javascript + function getData() { + return new Promise((resolve, reject) => { + fetch('/api/data') + .then(response => response.json()) + .then(data => resolve(data)) + .catch(error => reject(error)) + }) + } + ``` + + **Answer:** + + This is the **Promise constructor anti-pattern**. You're wrapping a Promise (`fetch`) inside `new Promise()` unnecessarily. Just return the Promise directly: + + ```javascript + function getData() { + return fetch('/api/data') + .then(response => response.json()) + } + ``` + + The original code: + - Adds unnecessary complexity + - Could lose stack trace information + - Might swallow errors if you forget the `.catch()` + + Only use `new Promise()` when wrapping callback-based APIs. + </Accordion> + + <Accordion title="Question 6: What's the output order?"> + ```javascript + console.log('A') + + Promise.resolve().then(() => console.log('B')) + + Promise.resolve().then(() => { + console.log('C') + Promise.resolve().then(() => console.log('D')) + }) + + console.log('E') + ``` + + **Answer:** `A`, `E`, `B`, `C`, `D` + + **Explanation:** + 1. `'A'` — Synchronous, runs first + 2. First `.then()` callback queued as microtask + 3. Second `.then()` callback queued as microtask + 4. `'E'` — Synchronous, runs next + 5. Synchronous code done → process microtask queue + 6. `'B'` — First microtask runs + 7. `'C'` — Second microtask runs, queues another microtask + 8. `'D'` — Third microtask runs (microtask queue is drained before any macrotask) + + Promise callbacks always run as microtasks, after the current synchronous code but before macrotasks like `setTimeout`. See [Event Loop](/concepts/event-loop) for more. + </Accordion> +</AccordionGroup> + +--- + +## Related Concepts + +<CardGroup cols={2}> + <Card title="Callbacks" icon="phone" href="/concepts/callbacks"> + The predecessor to Promises — understand what Promises improve upon + </Card> + <Card title="async/await" icon="hourglass" href="/concepts/async-await"> + Modern syntax built on top of Promises — makes async code look synchronous + </Card> + <Card title="Event Loop" icon="arrows-spin" href="/concepts/event-loop"> + How Promise callbacks are scheduled via the microtask queue + </Card> + <Card title="Fetch API" icon="globe" href="/concepts/http-fetch"> + The most common Promise-based API — making HTTP requests + </Card> +</CardGroup> + +--- + +## Reference + +<CardGroup cols={2}> + <Card title="Promise — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise"> + Complete reference for the Promise object and all its methods + </Card> + <Card title="Using Promises — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises"> + MDN guide covering Promise fundamentals and patterns + </Card> + <Card title="Promise.all() — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all"> + Documentation for Promise.all() with examples + </Card> + <Card title="Promise.allSettled() — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled"> + Documentation for Promise.allSettled() with examples + </Card> + <Card title="Promise.try() — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/try"> + Documentation for Promise.try() (Baseline 2025) + </Card> + <Card title="Promise.withResolvers() — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers"> + Documentation for Promise.withResolvers() (ES2024) + </Card> +</CardGroup> + +## Articles + +<CardGroup cols={2}> + <Card title="JavaScript Promises: An Introduction" icon="newspaper" href="https://web.dev/promises/"> + Google's web.dev tutorial with inline runnable code examples you can edit. Covers the full Promise API from basics to advanced patterns like promisification. + </Card> + <Card title="JavaScript Visualized: Promises & Async/Await" icon="newspaper" href="https://dev.to/lydiahallie/javascript-visualized-promises-async-await-5gke"> + Lydia Hallie's visual explanation with animated GIFs showing exactly how Promises work. + </Card> + <Card title="Promise Basics — JavaScript.info" icon="newspaper" href="https://javascript.info/promise-basics"> + The go-to reference for Promise fundamentals with the "loadScript" example that makes async patterns click. Includes exercises at the end to test your understanding. + </Card> + <Card title="Promise Chaining — JavaScript.info" icon="newspaper" href="https://javascript.info/promise-chaining"> + Excellent diagrams showing how values flow through Promise chains. The "returning promises" section clarifies the trickiest part of chaining. + </Card> + <Card title="We Have a Problem with Promises" icon="newspaper" href="https://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html"> + Nolan Lawson's classic article on common Promise mistakes developers make. + </Card> + <Card title="The Complete JavaScript Promise Guide" icon="newspaper" href="https://blog.webdevsimplified.com/2021-09/javascript-promises"> + Kyle Cook's written companion to his popular YouTube videos. Great if you prefer reading to watching, with the same clear teaching style. + </Card> +</CardGroup> + +## Videos + +<CardGroup cols={2}> + <Card title="JavaScript Promises In 10 Minutes" icon="video" href="https://www.youtube.com/watch?v=DHvZLI7Db8E"> + Perfect if you're short on time. Kyle covers creating, consuming, and chaining Promises with real code examples in just 10 minutes. + </Card> + <Card title="Promises — Fun Fun Function" icon="video" href="https://www.youtube.com/watch?v=2d7s3spWAzo"> + MPJ's entertaining and thorough explanation of Promises with great analogies. + </Card> + <Card title="JavaScript Promise in 100 Seconds" icon="video" href="https://www.youtube.com/watch?v=RvYYCGs45L4"> + Fireship's ultra-concise overview of Promise fundamentals. + </Card> + <Card title="Promises | Namaste JavaScript" icon="video" href="https://youtu.be/ap-6PPAuK1Y"> + Akshay walks through Promise internals with browser DevTools, showing exactly what happens at each step. Great for understanding the "why" behind Promises. + </Card> +</CardGroup> diff --git a/docs/concepts/pure-functions.mdx b/docs/concepts/pure-functions.mdx new file mode 100644 index 00000000..2d91ff01 --- /dev/null +++ b/docs/concepts/pure-functions.mdx @@ -0,0 +1,809 @@ +--- +title: "Pure Functions: Writing Predictable Code in JavaScript" +sidebarTitle: "Pure Functions: Writing Predictable Code" +description: "Learn pure functions in JavaScript. Understand the two rules of purity, avoid side effects, and write testable, predictable code with immutable patterns." +--- + +Why does the same function sometimes give you different results? Why is some code easy to test while other code requires elaborate setup and mocking? Why do bugs seem to appear "randomly" when your logic looks correct? + +The answer often comes down to **pure functions**. They're at the heart of functional programming, and understanding them will change how you write JavaScript. + +```javascript +// A pure function: same input always gives same output +function add(a, b) { + return a + b +} + +add(2, 3) // 5 +add(2, 3) // 5, always 5, no matter when or where you call it +``` + +A pure function is simple, predictable, and trustworthy. Once you understand why, you'll start seeing opportunities to write cleaner code everywhere. + +<Info> +**What you'll learn in this guide:** +- The two rules that make a function "pure" +- What side effects are and how they create bugs +- How to identify pure vs impure functions +- Practical patterns for avoiding mutations +- When pure functions aren't possible (and what to do instead) +- Why purity makes testing and debugging much easier +</Info> + +<Warning> +**Helpful background:** This guide references object and array mutations frequently. If you're not comfortable with how JavaScript handles [value vs reference types](/concepts/value-reference-types), read that guide first. It explains why `const arr = [1,2,3]; arr.push(4)` works but shouldn't surprise you. +</Warning> + +--- + +## What is a Pure Function? + +A **pure function** is a function that follows two simple rules: + +1. **Same input → Same output**: Given the same arguments, it always returns the same result +2. **No side effects**: It doesn't change anything outside itself + +That's it. If a function follows both rules, it's pure. If it breaks either rule, it's impure. + +```javascript +// ✓ PURE: Follows both rules +function double(x) { + return x * 2 +} + +double(5) // 10 +double(5) // 10, always 10 +``` + +Using [`Math.random()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random) breaks purity because it introduces randomness: + +```javascript +// ❌ IMPURE: Breaks rule 1 (different output for same input) +function randomDouble(x) { + return x * Math.random() +} + +randomDouble(5) // 2.3456... +randomDouble(5) // 4.1234... different every time! +``` + +```javascript +// ❌ IMPURE: Breaks rule 2 (has a side effect) +let total = 0 + +function addToTotal(x) { + total += x // Modifies external variable! + return total +} + +addToTotal(5) // 5 +addToTotal(5) // 10. Different result because total changed! +``` + +<CardGroup cols={2}> + <Card title="Functions — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Functions"> + MDN guide covering JavaScript function fundamentals + </Card> + <Card title="Functional Programming — Wikipedia" icon="book" href="https://en.wikipedia.org/wiki/Pure_function"> + Formal definition of pure functions in computer science + </Card> +</CardGroup> + +--- + +## The Kitchen Recipe Analogy + +Think of a pure function like a recipe. If you give a recipe the same ingredients, you get the same dish every time. The recipe doesn't care what time it is, what else is in your kitchen, or what you cooked yesterday. + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ PURE VS IMPURE FUNCTIONS │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ PURE FUNCTION (Like a Recipe) │ +│ ───────────────────────────── │ +│ │ +│ Ingredients Recipe Dish │ +│ ┌───────────┐ ┌─────────┐ ┌───────┐ │ +│ │ 2 eggs │ │ │ │ │ │ +│ │ flour │ ────► │ mix & │ ────► │ cake │ │ +│ │ sugar │ │ bake │ │ │ │ +│ └───────────┘ └─────────┘ └───────┘ │ +│ │ +│ ✓ Same ingredients = Same cake, every time │ +│ ✓ Doesn't rearrange your kitchen │ +│ ✓ Doesn't depend on the weather │ +│ │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ IMPURE FUNCTION (Unpredictable Chef) │ +│ ──────────────────────────────────── │ +│ │ +│ ┌───────────┐ ┌─────────┐ ┌───────┐ │ +│ │ 2 eggs │ │ checks │ │ ??? │ │ +│ │ flour │ ────► │ clock, │ ────► │ │ │ +│ │ sugar │ │ mood... │ │ │ │ +│ └───────────┘ └─────────┘ └───────┘ │ +│ │ +│ ✗ Same ingredients might give different results │ +│ ✗ Might rearrange your whole kitchen while cooking │ +│ ✗ Depends on external factors you can't control │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +A pure function is like a recipe: predictable, self-contained, and trustworthy. An impure function is like a chef who checks the weather, changes the recipe based on mood, and rearranges your kitchen while cooking. + +--- + +## Rule 1: Same Input → Same Output + +This rule is also called **referential transparency**. It means you could replace a function call with its result and the program would work exactly the same. + +[`Math.max()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/max) is a great example of a pure function: + +```javascript +// ✓ PURE: Math.max always returns the same result for the same inputs +Math.max(2, 8, 5) // 8 +Math.max(2, 8, 5) // 8, always 8 + +// You could replace Math.max(2, 8, 5) with 8 anywhere in your code +// and nothing would change. That's referential transparency. +``` + +### What Breaks This Rule? + +Anything that makes the output depend on something other than the inputs: + +<Tabs> + <Tab title="Random Values"> + ```javascript + // ❌ IMPURE: Output depends on randomness + function getRandomDiscount(price) { + return price * Math.random() + } + +getRandomDiscount(100) // 47.23... +getRandomDiscount(100) // 82.91... different! + ``` + </Tab> + <Tab title="Current Time"> + Using [`new Date()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) makes functions impure because the output depends on when you call them: + + ```javascript + // ❌ IMPURE: Output depends on when you call it + function getGreeting(name) { + const hour = new Date().getHours() + if (hour < 12) return `Good morning, ${name}` + return `Good afternoon, ${name}` + } + + // Same input, different output depending on time of day + ``` + </Tab> + <Tab title="External State"> + ```javascript + // ❌ IMPURE: Output depends on external variable + let taxRate = 0.08 + + function calculateTotal(price) { + return price + (price * taxRate) + } + + calculateTotal(100) // 108 + taxRate = 0.10 + calculateTotal(100) // 110. Different! + ``` + </Tab> +</Tabs> + +### How to Fix It + +Pass everything the function needs as arguments: + +```javascript +// ✓ PURE: Tax rate is now an input, not external state +function calculateTotal(price, taxRate) { + return price + (price * taxRate) +} + +calculateTotal(100, 0.08) // 108 +calculateTotal(100, 0.08) // 108, always the same +calculateTotal(100, 0.10) // 110 — different input, different output (that's fine!) +``` + +<Tip> +**Quick test for Rule 1:** Can you predict the output just by looking at the inputs? If you need to know the current time, check a global variable, or run it to find out, it's probably not pure. +</Tip> + +--- + +## Rule 2: No Side Effects + +A **side effect** is anything a function does besides computing and returning a value. Side effects are actions that affect the world outside the function. + +### Common Side Effects + +| Side Effect | Example | Why It's a Problem | +|-------------|---------|-------------------| +| **Mutating input** | `array.push(item)` | Changes data the caller might still be using | +| **Modifying external variables** | `counter++` | Creates hidden dependencies | +| **Console output** | `console.log()` | Does something besides returning a value | +| **DOM manipulation** | `element.innerHTML = '...'` | Changes the page state | +| **HTTP requests** | `fetch('/api/data')` | Communicates with external systems | +| **Writing to storage** | `localStorage.setItem()` | Persists data outside the function | +| **Throwing exceptions** | `throw new Error()` | Breaks normal control flow (debated) | + +```javascript +// ❌ IMPURE: Multiple side effects +function processUser(user) { + user.lastLogin = new Date() // Side effect: mutates input + console.log(`User ${user.name}`) // Side effect: console output + userCount++ // Side effect: modifies external variable + return user +} + +// ✓ PURE: Returns new data, no side effects +function processUser(user, loginTime) { + return { + ...user, + lastLogin: loginTime + } +} +``` + +<Note> +**Is `console.log()` really that bad?** Technically, yes. It's a side effect. But practically? It's fine for debugging. The key is understanding that it makes your function impure. Don't let `console.log` statements slip into production code that should be pure. +</Note> + +--- + +## The #1 Pure Functions Mistake: Mutations + +The most common way developers accidentally create impure functions is by **mutating objects or arrays** that were passed in. + +```javascript +// ❌ IMPURE: Mutates the input array +function addItem(cart, item) { + cart.push(item) // This changes the original cart! + return cart +} + +const myCart = ['apple', 'banana'] +const newCart = addItem(myCart, 'orange') + +console.log(myCart) // ['apple', 'banana', 'orange'] — Original changed! +console.log(newCart) // ['apple', 'banana', 'orange'] +console.log(myCart === newCart) // true — They're the same array! +``` + +This creates bugs because any other code using `myCart` now sees unexpected changes. The fix is simple: return a **new** array instead of modifying the original. + +```javascript +// ✓ PURE: Returns a new array, original unchanged +function addItem(cart, item) { + return [...cart, item] // Spread into new array +} + +const myCart = ['apple', 'banana'] +const newCart = addItem(myCart, 'orange') + +console.log(myCart) // ['apple', 'banana'] — Original unchanged! +console.log(newCart) // ['apple', 'banana', 'orange'] +console.log(myCart === newCart) // false — Different arrays +``` + +### Shallow Copy Trap + +Watch out: the [spread operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax) only creates a **shallow copy**. Nested objects are still shared: + +```javascript +// ⚠️ DANGER: Shallow copy with nested objects +const user = { + name: 'Alice', + address: { city: 'NYC', zip: '10001' } +} + +const updatedUser = { ...user, name: 'Bob' } + +// Top level is a new object... +console.log(user === updatedUser) // false ✓ + +// But nested object is SHARED +updatedUser.address.city = 'LA' +console.log(user.address.city) // 'LA'. Original changed! +``` + +For nested objects, use [`structuredClone()`](https://developer.mozilla.org/en-US/docs/Web/API/structuredClone) for a deep copy: + +```javascript +// ✓ SAFE: Deep clone for nested objects +const user = { + name: 'Alice', + address: { city: 'NYC', zip: '10001' } +} + +const updatedUser = { + ...structuredClone(user), + name: 'Bob' +} + +updatedUser.address.city = 'LA' +console.log(user.address.city) // 'NYC' — Original safe! +``` + +<Note> +**Limitation:** `structuredClone()` cannot clone functions or DOM nodes. It will throw a `DataCloneError` for these types. +</Note> + +<Warning> +**The Trap:** Spread operator (`...`) only copies one level deep. If you have nested objects or arrays, mutations to the nested data will affect the original. Use `structuredClone()` for deep copies, or see our [Value vs Reference Types](/concepts/value-reference-types) guide for more patterns. +</Warning> + +--- + +## Immutable Patterns for Pure Functions + +Here are the most common patterns for writing pure functions that handle objects and arrays: + +### Updating Objects + +```javascript +// ❌ IMPURE: Mutates the object +function updateEmail(user, email) { + user.email = email + return user +} + +// ✓ PURE: Returns new object with updated property +function updateEmail(user, email) { + return { ...user, email } +} +``` + +### Adding to Arrays + +```javascript +// ❌ IMPURE: Mutates the array +function addTodo(todos, newTodo) { + todos.push(newTodo) + return todos +} + +// ✓ PURE: Returns new array with item added +function addTodo(todos, newTodo) { + return [...todos, newTodo] +} +``` + +### Removing from Arrays + +```javascript +// ❌ IMPURE: Mutates the array +function removeTodo(todos, index) { + todos.splice(index, 1) + return todos +} + +// ✓ PURE: Returns new array without the item +function removeTodo(todos, index) { + return todos.filter((_, i) => i !== index) +} +``` + +### Updating Array Items + +```javascript +// ❌ IMPURE: Mutates item in array +function completeTodo(todos, index) { + todos[index].completed = true + return todos +} + +// ✓ PURE: Returns new array with updated item +function completeTodo(todos, index) { + return todos.map((todo, i) => + i === index ? { ...todo, completed: true } : todo + ) +} +``` + +### Sorting Arrays + +```javascript +// ❌ IMPURE: sort() mutates the original array! +function getSorted(numbers) { + return numbers.sort((a, b) => a - b) +} + +// ✓ PURE: Copy first, then sort +function getSorted(numbers) { + return [...numbers].sort((a, b) => a - b) +} + +// ✓ PURE (ES2023+): Use toSorted() which returns a new array +function getSorted(numbers) { + return numbers.toSorted((a, b) => a - b) +} +``` + +<Tip> +**ES2023 added non-mutating versions** of several array methods: `toSorted()`, `toReversed()`, `toSpliced()`, and `with()`. These are perfect for pure functions. Check browser support before using in production. +</Tip> + +--- + +## Why Pure Functions Matter + +Writing pure functions isn't just about following rules. It brings real, practical benefits: + +<AccordionGroup> + <Accordion title="1. Easier to Test"> + Pure functions are a testing dream. No mocking, no setup, no cleanup. Just call the function and check the result. + + ```javascript + // Testing a pure function is trivial + function add(a, b) { + return a + b + } + + // Test + expect(add(2, 3)).toBe(5) + expect(add(-1, 1)).toBe(0) + expect(add(0, 0)).toBe(0) + // Done! No mocks, no setup, no teardown + ``` + + Compare this to testing a function that reads from the DOM, makes API calls, or depends on global state. You'd need elaborate setup just to run one test. + </Accordion> + + <Accordion title="2. Easier to Debug"> + When something goes wrong, pure functions narrow down the problem fast. If a pure function returns the wrong value, the bug is in *that function*. It can't be caused by some other code changing global state. + + ```javascript + // If calculateTax(100, 0.08) returns the wrong value, + // the bug MUST be inside calculateTax. + // No need to check what other code ran before it. + function calculateTax(amount, rate) { + return amount * rate + } + ``` + </Accordion> + + <Accordion title="3. Safe to Cache (Memoization)"> + Since pure functions always return the same output for the same input, you can safely cache their results. This is called memoization. + + ```javascript + // Expensive calculation - safe to cache because it's pure + function fibonacci(n) { + if (n <= 1) return n + return fibonacci(n - 1) + fibonacci(n - 2) + } + + // With memoization, fibonacci(40) computes once, then returns cached result + ``` + + You can't safely cache impure functions because they might need to return different values even with the same inputs. + </Accordion> + + <Accordion title="4. Safe to Parallelize"> + Pure functions don't depend on shared state, so they can safely run in parallel. This is how libraries like TensorFlow process massive datasets across multiple CPU cores or GPU threads. + + ```javascript + // These can all run at the same time - no conflicts! + const results = await Promise.all([ + processChunk(data.slice(0, 1000)), + processChunk(data.slice(1000, 2000)), + processChunk(data.slice(2000, 3000)) + ]) + ``` + </Accordion> + + <Accordion title="5. Easier to Understand"> + When you see a pure function, you know everything it can do is right there in the code. No hidden dependencies, no spooky action at a distance. + + ```javascript + // You can understand this function completely by reading it + function formatPrice(cents, currency = 'USD') { + const dollars = cents / 100 + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency + }).format(dollars) + } + ``` + + This function uses [`Intl.NumberFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat) but remains pure because the same inputs always produce the same formatted output. + </Accordion> +</AccordionGroup> + +--- + +## When Pure Functions Aren't Possible + +Let's be realistic: you can't build useful applications with *only* pure functions. At some point you need to: + +- Read from and write to the DOM +- Make HTTP requests +- Log errors +- Save to localStorage +- Respond to user events + +The strategy is to **push impure code to the edges** of your application. Keep the core logic pure, and isolate the impure parts. + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ STRUCTURE OF A WELL-DESIGNED APP │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ EDGES (Impure) CORE (Pure) EDGES (Impure) │ +│ ────────────── ────────── ────────────── │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Read from │ │ Transform │ │ Write to │ │ +│ │ DOM, API, │ ──────► │ Calculate │ ──────► │ DOM, API, │ │ +│ │ user input │ │ Process │ │ console │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +│ │ +│ INPUT LOGIC OUTPUT │ +│ (impure) (pure) (impure) │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### Example: Separating Pure from Impure + +```javascript +// IMPURE: Reads from DOM +function getUserInput() { + return document.querySelector('#username').value +} + +// PURE: Transforms data (no DOM access) +function formatUsername(name) { + return name.trim().toLowerCase() +} + +// PURE: Validates data (no side effects) +function isValidUsername(name) { + return name.length >= 3 && name.length <= 20 +} + +// IMPURE: Writes to DOM +function displayError(message) { + document.querySelector('#error').textContent = message +} + +// Orchestration: impure code at the edges +function handleSubmit() { + const raw = getUserInput() // Impure: read + const formatted = formatUsername(raw) // Pure: transform + const isValid = isValidUsername(formatted) // Pure: validate + + if (!isValid) { + displayError('Username must be 3-20 characters') // Impure: write + } +} +``` + +The pure functions (`formatUsername`, `isValidUsername`) are easy to test and reuse. The impure functions are isolated at the edges where they're easy to find and manage. + +--- + +## Key Takeaways + +<Info> +**The key things to remember about pure functions:** + +1. **Two rules define purity**: same input → same output, and no side effects + +2. **Side effects** include mutations, console.log, DOM access, HTTP requests, randomness, and current time + +3. **Mutations are the #1 trap**. Use spread operator or `structuredClone()` to return new data instead + +4. **Shallow copies aren't enough** for nested objects. The spread operator only copies one level deep + +5. **Pure functions are easier to test**. No mocking, no setup. Just input and expected output + +6. **Pure functions are easier to debug**. If the output is wrong, the bug is in that function + +7. **Pure functions can be cached**. Same input always means same output, so memoization is safe + +8. **You can't avoid impurity entirely**. The goal is to isolate it at the edges of your application + +9. **console.log is technically impure** but acceptable for debugging. Just don't let it slip into logic that should be pure + +10. **ES2023 added `toSorted()`, `toReversed()`** and other non-mutating array methods. Use them when you can! +</Info> + +--- + +## Test Your Knowledge + +<AccordionGroup> + <Accordion title="Question 1: What two rules define a pure function?"> + **Answer:** + + A pure function must follow both rules: + + 1. **Same input → Same output**: Given the same arguments, it always returns the same result (referential transparency) + 2. **No side effects**: It doesn't modify anything outside itself (no mutations, no I/O, no external state changes) + + ```javascript + // Pure: follows both rules + function multiply(a, b) { + return a * b + } + ``` + </Accordion> + + <Accordion title="Question 2: Is this function pure? Why or why not?"> + ```javascript + function greet(name) { + return `Hello, ${name}! The time is ${new Date().toLocaleTimeString()}` + } + ``` + + **Answer:** + + No, this function is **impure**. It breaks Rule 1 (same input → same output) because it uses `new Date()`. Calling `greet('Alice')` at 10:00 AM gives a different result than calling it at 3:00 PM, even though the input is the same. + + To make it pure, pass the time as a parameter: + + ```javascript + function greet(name, time) { + return `Hello, ${name}! The time is ${time}` + } + ``` + </Accordion> + + <Accordion title="Question 3: What's wrong with this function?"> + ```javascript + function addToCart(cart, item) { + cart.push(item) + return cart + } + ``` + + **Answer:** + + This function **mutates its input**. The `push()` method modifies the original `cart` array, which is a side effect. Any other code using that cart array will see unexpected changes. + + Fix it by returning a new array: + + ```javascript + function addToCart(cart, item) { + return [...cart, item] + } + ``` + </Accordion> + + <Accordion title="Question 4: How do you safely update a nested object in a pure function?"> + **Answer:** + + Use `structuredClone()` for a deep copy, or carefully spread at each level: + + ```javascript + // Option 1: structuredClone (simplest) + function updateCity(user, newCity) { + const copy = structuredClone(user) + copy.address.city = newCity + return copy + } + + // Option 2: Spread at each level + function updateCity(user, newCity) { + return { + ...user, + address: { + ...user.address, + city: newCity + } + } + } + ``` + + Note: A simple `{ ...user }` shallow copy would still share the nested `address` object with the original. + </Accordion> + + <Accordion title="Question 5: Why are pure functions easier to test?"> + **Answer:** + + Pure functions only depend on their inputs and only produce their return value. This means: + + - **No setup needed**: You don't need to configure global state, mock APIs, or set up DOM elements + - **No cleanup needed**: The function doesn't change anything, so there's nothing to reset + - **Predictable**: Same input always gives same output, so tests are deterministic + - **Isolated**: If a test fails, the bug must be in that function + + ```javascript + // Testing a pure function - simple and straightforward + expect(add(2, 3)).toBe(5) + expect(formatName(' ALICE ')).toBe('alice') + expect(isValidEmail('test@example.com')).toBe(true) + ``` + </Accordion> + + <Accordion title="Question 6: When is it okay to have impure functions?"> + **Answer:** + + Impure functions are necessary for any real application. You need them to: + + - Read user input from the DOM + - Make HTTP requests to APIs + - Write output to the screen + - Save data to localStorage or databases + - Log errors and debugging info + + The strategy is to **isolate impurity at the edges** of your application. Keep your core business logic in pure functions, and use impure functions only for I/O operations. This gives you the best of both worlds: testable, predictable logic with the ability to interact with the outside world. + </Accordion> +</AccordionGroup> + +--- + +## Related Concepts + +<CardGroup cols={2}> + <Card title="Value vs Reference Types" icon="diagram-project" href="/concepts/value-reference-types"> + Understanding mutations, shallow vs deep copies, and why objects behave differently than primitives + </Card> + <Card title="Higher-Order Functions" icon="arrows-repeat" href="/concepts/higher-order-functions"> + Functions that take or return functions, perfect for composing pure functions + </Card> + <Card title="map, reduce & filter" icon="filter" href="/concepts/map-reduce-filter"> + Non-mutating array methods that return new arrays, ideal for pure functions + </Card> + <Card title="Currying & Composition" icon="wand-magic-sparkles" href="/concepts/currying-composition"> + Advanced patterns for building complex pure functions from simple ones + </Card> +</CardGroup> + +--- + +## Reference + +<CardGroup cols={2}> + <Card title="Array Methods — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array"> + Complete reference for all array methods, including which ones mutate + </Card> + <Card title="Spread Syntax — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax"> + How to use the spread operator for shallow copies of arrays and objects + </Card> + <Card title="structuredClone() — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/structuredClone"> + Modern API for deep cloning objects, including nested structures + </Card> + <Card title="Object.freeze() — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze"> + How to make objects immutable (though only shallowly) + </Card> +</CardGroup> + +## Articles + +<CardGroup cols={2}> + <Card title="What Is a Pure Function in JavaScript?" icon="newspaper" href="https://www.freecodecamp.org/news/what-is-a-pure-function-in-javascript-acb887375dfe/"> + Yazeed Bzadough's checklist approach with clear examples. Perfect starting point for understanding the two rules of pure functions and common side effects. + </Card> + <Card title="Master the JavaScript Interview: What is a Pure Function?" icon="newspaper" href="https://medium.com/javascript-scene/master-the-javascript-interview-what-is-a-pure-function-d1c076bec976"> + Eric Elliott's deep dive into referential transparency and shared state. Includes real-world examples of race conditions caused by impure functions. + </Card> + <Card title="Making your JavaScript Pure" icon="newspaper" href="https://alistapart.com/article/making-your-javascript-pure/"> + Jack Franklin's practical guide focusing on testability. Excellent "before and after" refactoring examples that show how to transform impure code. + </Card> + <Card title="How to Deal with Dirty Side Effects in Pure Functional JavaScript" icon="newspaper" href="https://jrsinclair.com/articles/2018/how-to-deal-with-dirty-side-effects-in-your-pure-functional-javascript/"> + James Sinclair's advanced guide to dependency injection and the Effect pattern. For when you're ready to take functional programming to the next level. + </Card> +</CardGroup> + +## Videos + +<CardGroup cols={2}> + <Card title="Refactoring Into Pure Functions — Fun Fun Function" icon="video" href="https://www.youtube.com/watch?v=cUrEedgvJSk"> + Mattias Petter Johansson demonstrates refactoring JavaScript code into tiny, pure, composable functions. Part of his excellent functional programming series that makes complex topics approachable. + </Card> + <Card title="Pure vs Impure Functions" icon="video" href="https://www.youtube.com/watch?v=AHbRVJzpB54"> + Theodore Anderson's clear comparison of pure and impure functions with practical JavaScript examples and visual explanations. + </Card> + <Card title="JavaScript Pure Functions" icon="video" href="https://www.youtube.com/watch?v=frT3H-eBmPc"> + Seth Alexander's focused tutorial covering the fundamentals of pure functions and their benefits for writing maintainable code. + </Card> +</CardGroup> diff --git a/docs/concepts/recursion.mdx b/docs/concepts/recursion.mdx new file mode 100644 index 00000000..b6d129e9 --- /dev/null +++ b/docs/concepts/recursion.mdx @@ -0,0 +1,1049 @@ +--- +title: "Recursion: Functions That Call Themselves in JavaScript" +sidebarTitle: "Recursion: Functions That Call Themselves" +description: "Learn recursion in JavaScript. Understand base cases, recursive calls, the call stack, and patterns like factorial, tree traversal, and memoization." +--- + +How do you solve a problem by breaking it into smaller versions of the same problem? What if a function could call itself to chip away at a task until it's done? + +```javascript +function countdown(n) { + if (n === 0) { + console.log("Done!") + return + } + console.log(n) + countdown(n - 1) // The function calls itself! +} + +countdown(3) +// 3 +// 2 +// 1 +// Done! +``` + +This is **[recursion](https://developer.mozilla.org/en-US/docs/Glossary/Recursion)**. The `countdown` function calls itself with a smaller number each time until it reaches zero. It's a powerful technique that lets you solve complex problems by breaking them into simpler, self-similar pieces. + +<Info> +**What you'll learn in this guide:** +- What recursion is and its two essential parts (base case and recursive case) +- How recursion relates to the call stack +- Classic recursive algorithms: factorial, Fibonacci, sum +- Practical applications: traversing trees, nested objects, linked lists +- Recursion vs iteration: when to use each +- Common mistakes and how to avoid stack overflow +- Optimization techniques: memoization and tail recursion +</Info> + +<Warning> +**Prerequisite:** This guide assumes you understand [JavaScript functions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Functions). It also helps to know how the [call stack](/concepts/call-stack) works, though we'll cover that relationship here. +</Warning> + +--- + +## What is Recursion? + +**[Recursion](https://developer.mozilla.org/en-US/docs/Glossary/Recursion)** is a programming technique where a function calls itself to solve a problem. Instead of using loops, the function breaks a problem into smaller versions of the same problem until it reaches a case simple enough to solve directly. + +<Note> +**Recursion isn't unique to JavaScript.** It's a fundamental computer science concept found in virtually every programming language. The patterns you learn here apply whether you're writing Python, C++, Java, or any other language. +</Note> + +Every recursive function has two essential parts: + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ THE TWO PARTS OF RECURSION │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ function solve(problem) { │ +│ │ +│ if (problem is simple enough) { ← BASE CASE │ +│ return solution directly Stops the recursion │ +│ } │ +│ │ +│ return solve(smaller problem) ← RECURSIVE CASE │ +│ } Calls itself with simpler input│ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +1. **Base Case**: The condition that stops the recursion. Without it, the function would call itself forever. + +2. **Recursive Case**: The part where the function calls itself with a simpler or smaller version of the problem. + +Here's a simple example that sums numbers from 1 to `n`: + +```javascript +function sumTo(n) { + // Base case: when n is 1 or less, return n + if (n <= 1) { + return n + } + + // Recursive case: n plus the sum of everything below it + return n + sumTo(n - 1) +} + +console.log(sumTo(5)) // 15 (5 + 4 + 3 + 2 + 1) +console.log(sumTo(1)) // 1 +console.log(sumTo(0)) // 0 +``` + +The function asks: "What's the sum from 1 to 5?" It answers: "5 plus the sum from 1 to 4." Then it asks the same question with 4, then 3, then 2, until it reaches 1, which it knows is just 1. + +--- + +## The Russian Dolls Analogy + +Think of recursion like opening a set of Russian nesting dolls (matryoshka). Each doll contains a smaller version of itself, and you keep opening them until you reach the smallest one that can't be opened. + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ THE RUSSIAN DOLLS ANALOGY │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Opening the dolls (making recursive calls): │ +│ │ +│ ╔═══════════════════════════════╗ │ +│ ║ ║ │ +│ ║ ╔═══════════════════════╗ ║ │ +│ ║ ║ ║ ║ │ +│ ║ ║ ╔═══════════════╗ ║ ║ │ +│ ║ ║ ║ ║ ║ ║ │ +│ ║ ║ ║ ╔═══════╗ ║ ║ ║ │ +│ ║ ║ ║ ║ ◆ ║ ║ ║ ║ ← Smallest doll (BASE CASE) │ +│ ║ ║ ║ ╚═══════╝ ║ ║ ║ Can't open further │ +│ ║ ║ ║ ║ ║ ║ │ +│ ║ ║ ╚═══════════════╝ ║ ║ │ +│ ║ ║ ║ ║ │ +│ ║ ╚═══════════════════════╝ ║ │ +│ ║ ║ │ +│ ╚═══════════════════════════════╝ │ +│ │ +│ Each doll = a function call │ +│ Opening a doll = making a recursive call │ +│ Smallest doll = base case (stop recursing) │ +│ Closing dolls back up = returning values back up the chain │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +When you find the smallest doll, you start closing them back up. In recursion, once you hit the base case, the return values bubble back up through each function call until you get your final answer. + +--- + +## How Recursion Works Under the Hood + +To understand recursion, you need to understand how the [call stack](/concepts/call-stack) works. Every time a function is called, JavaScript creates an **[execution context](https://developer.mozilla.org/en-US/docs/Glossary/Call_stack)** and pushes it onto the call stack. When the function returns, its context is popped off. + +With recursion, multiple execution contexts for the *same function* stack up: + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ THE CALL STACK DURING RECURSION │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ sumTo(3) calls sumTo(2) calls sumTo(1) │ +│ │ +│ STACK GROWING: STACK SHRINKING: │ +│ ─────────────── ───────────────── │ +│ │ +│ ┌───────────────┐ ┌───────────────┐ │ +│ │ sumTo(1) │ ← current │ │ (popped) │ +│ │ returns 1 │ └───────────────┘ │ +│ ├───────────────┤ ┌───────────────┐ │ +│ │ sumTo(2) │ │ sumTo(2) │ ← current │ +│ │ waiting... │ │ returns 2+1=3│ │ +│ ├───────────────┤ ├───────────────┤ │ +│ │ sumTo(3) │ │ sumTo(3) │ │ +│ │ waiting... │ │ waiting... │ │ +│ └───────────────┘ └───────────────┘ │ +│ │ +│ Each call waits for the one above it to return │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +Let's trace through `sumTo(3)` step by step: + +<Steps> + <Step title="sumTo(3) is called"> + `n` is 3, not 1, so we need to calculate `3 + sumTo(2)`. But we can't add yet because we don't know what `sumTo(2)` returns. This call waits. + </Step> + <Step title="sumTo(2) is called"> + `n` is 2, not 1, so we need `2 + sumTo(1)`. This call also waits. + </Step> + <Step title="sumTo(1) is called — base case!"> + `n` is 1, so we return `1` immediately. No more recursive calls. + </Step> + <Step title="sumTo(2) resumes"> + Now it knows `sumTo(1)` returned 1, so it calculates `2 + 1 = 3` and returns 3. + </Step> + <Step title="sumTo(3) resumes"> + Now it knows `sumTo(2)` returned 3, so it calculates `3 + 3 = 6` and returns 6. + </Step> +</Steps> + +```javascript +function sumTo(n) { + console.log(`Called sumTo(${n})`) + + if (n === 1) { + console.log(` Base case! Returning 1`) + return 1 + } + + const result = n + sumTo(n - 1) + console.log(` sumTo(${n}) returning ${result}`) + return result +} + +sumTo(3) +// Called sumTo(3) +// Called sumTo(2) +// Called sumTo(1) +// Base case! Returning 1 +// sumTo(2) returning 3 +// sumTo(3) returning 6 +``` + +<Tip> +**Key insight:** Each recursive call creates its own copy of the function's local variables. The `n` in `sumTo(3)` is separate from the `n` in `sumTo(2)`. They don't interfere with each other. +</Tip> + +--- + +## Classic Recursive Patterns + +Here are the most common recursive algorithms you'll encounter. Understanding these patterns will help you recognize when recursion is the right tool. + +<Note> +The examples below assume valid, non-negative integer inputs. In production code, you'd want to validate inputs and handle edge cases like negative numbers or non-integers. +</Note> + +<AccordionGroup> + <Accordion title="Factorial (n!)"> + The factorial of a number `n` (written as `n!`) is the product of all positive integers from 1 to n: + + - `5! = 5 × 4 × 3 × 2 × 1 = 120` + - `3! = 3 × 2 × 1 = 6` + - `1! = 1` + - `0! = 1` (by definition) + + The recursive insight: `n! = n × (n-1)!` + + ```javascript + function factorial(n) { + // Base case: 0! and 1! both equal 1 + if (n <= 1) { + return 1 + } + + // Recursive case: n! = n × (n-1)! + return n * factorial(n - 1) + } + + console.log(factorial(5)) // 120 + console.log(factorial(0)) // 1 + console.log(factorial(1)) // 1 + ``` + + **Trace of `factorial(4)`:** + ``` + factorial(4) = 4 * factorial(3) + = 4 * (3 * factorial(2)) + = 4 * (3 * (2 * factorial(1))) + = 4 * (3 * (2 * 1)) + = 4 * (3 * 2) + = 4 * 6 + = 24 + ``` + </Accordion> + + <Accordion title="Fibonacci Sequence"> + The Fibonacci sequence starts with 0 and 1, and each subsequent number is the sum of the two before it: + + `0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...` + + The recursive definition: + - `fib(0) = 0` + - `fib(1) = 1` + - `fib(n) = fib(n-1) + fib(n-2)` for n > 1 + + ```javascript + function fibonacci(n) { + // Base cases + if (n === 0) return 0 + if (n === 1) return 1 + + // Recursive case: sum of two preceding numbers + return fibonacci(n - 1) + fibonacci(n - 2) + } + + console.log(fibonacci(0)) // 0 + console.log(fibonacci(1)) // 1 + console.log(fibonacci(6)) // 8 + console.log(fibonacci(10)) // 55 + ``` + + <Warning> + **Performance trap!** This naive implementation is very slow for large numbers. `fibonacci(40)` makes over 300 million function calls because it recalculates the same values repeatedly. We'll fix this with memoization later. + </Warning> + + ``` + fibonacci(5) calls: + fib(5) + / \ + fib(4) fib(3) ← fib(3) calculated twice! + / \ / \ + fib(3) fib(2) fib(2) fib(1) + / \ + fib(2) fib(1) + ``` + </Accordion> + + <Accordion title="Sum of Numbers (1 to n)"> + Sum all integers from 1 to n: + + ```javascript + function sumTo(n) { + if (n <= 1) return n + return n + sumTo(n - 1) + } + + console.log(sumTo(5)) // 15 (1+2+3+4+5) + console.log(sumTo(100)) // 5050 + console.log(sumTo(0)) // 0 + ``` + + **Note:** There's a mathematical formula for this: `n * (n + 1) / 2`, which is O(1) instead of O(n). For simple sums, the formula is better. But the recursive approach teaches the pattern. + </Accordion> + + <Accordion title="Power Function (x^n)"> + Calculate `x` raised to the power of `n`: + + ```javascript + function power(x, n) { + // Base case: anything to the power of 0 is 1 + if (n === 0) return 1 + + // Recursive case: x^n = x * x^(n-1) + return x * power(x, n - 1) + } + + console.log(power(2, 0)) // 1 + console.log(power(2, 3)) // 8 + console.log(power(2, 10)) // 1024 + console.log(power(3, 4)) // 81 + ``` + + **Optimized version** using the property that `x^n = (x^(n/2))^2`: + + ```javascript + function powerFast(x, n) { + if (n === 0) return 1 + + if (n % 2 === 0) { + // Even exponent: x^n = (x^(n/2))^2 + const half = powerFast(x, n / 2) + return half * half + } else { + // Odd exponent: x^n = x * x^(n-1) + return x * powerFast(x, n - 1) + } + } + + console.log(powerFast(2, 10)) // 1024 (but faster!) + ``` + + The optimized version runs in O(log n) time instead of O(n). + </Accordion> + + <Accordion title="Reverse a String"> + Reverse a string character by character: + + ```javascript + function reverse(str) { + // Base case: empty string or single character + if (str.length <= 1) { + return str + } + + // Recursive case: last char + reverse of the rest + return str[str.length - 1] + reverse(str.slice(0, -1)) + } + + console.log(reverse("hello")) // "olleh" + console.log(reverse("a")) // "a" + console.log(reverse("")) // "" + ``` + + **How it works:** + ``` + reverse("cat") + = "t" + reverse("ca") + = "t" + ("a" + reverse("c")) + = "t" + ("a" + "c") + = "t" + "ac" + = "tac" + ``` + </Accordion> +</AccordionGroup> + +--- + +## Practical Use Cases + +Recursion really shines when working with **nested or tree-like structures**. These [data structures](/concepts/data-structures) are naturally recursive, and recursion is often the most elegant solution. + +### Traversing Nested Objects + +Imagine you need to find all values in a deeply nested object: + +```javascript +const data = { + name: "Company", + departments: { + engineering: { + frontend: { count: 5 }, + backend: { count: 8 } + }, + sales: { count: 12 } + } +} + +function findAllCounts(obj) { + let total = 0 + + for (const key in obj) { + if (key === "count") { + total += obj[key] + } else if (typeof obj[key] === "object" && obj[key] !== null) { + // Recurse into nested objects + total += findAllCounts(obj[key]) + } + } + + return total +} + +console.log(findAllCounts(data)) // 25 +``` + +Without recursion, you'd need to know exactly how deep the nesting goes. With recursion, it handles any depth automatically. + +### Flattening Nested Arrays + +Turn a deeply nested array into a flat one: + +```javascript +function flatten(arr) { + let result = [] + + for (const item of arr) { + if (Array.isArray(item)) { + // Recurse into nested arrays + result = result.concat(flatten(item)) + } else { + result.push(item) + } + } + + return result +} + +console.log(flatten([1, [2, [3, 4]], 5])) +// [1, 2, 3, 4, 5] + +console.log(flatten([1, [2, [3, [4, [5]]]]])) +// [1, 2, 3, 4, 5] +``` + +### Walking the DOM Tree + +Traverse all elements in an HTML document: + +```javascript +function walkDOM(node, callback) { + // Process this node + callback(node) + + // Recurse into child nodes + for (const child of node.children) { + walkDOM(child, callback) + } +} + +// Example: log all tag names +walkDOM(document.body, (node) => { + console.log(node.tagName) +}) +``` + +This pattern combines recursion with [higher-order functions](/concepts/higher-order-functions) (the callback). It's how browser developer tools display the DOM tree and how libraries traverse HTML structures. + +### Processing Linked Lists + +A linked list is a classic recursive data structure where each node points to the next: + +```javascript +const list = { + value: 1, + next: { + value: 2, + next: { + value: 3, + next: null + } + } +} + +// Sum all values in the list +function sumList(node) { + if (node === null) return 0 + return node.value + sumList(node.next) +} + +console.log(sumList(list)) // 6 + +// Print list in reverse order +function printReverse(node) { + if (node === null) return + printReverse(node.next) // First, go to the end + console.log(node.value) // Then print on the way back +} + +printReverse(list) +// 3 +// 2 +// 1 +``` + +### File System Traversal + +A conceptual example of how file explorers work: + +```javascript +// Simulated file structure +const fileSystem = { + name: "root", + type: "folder", + children: [ + { name: "file1.txt", type: "file", size: 100 }, + { + name: "docs", + type: "folder", + children: [ + { name: "readme.md", type: "file", size: 50 }, + { name: "notes.txt", type: "file", size: 25 } + ] + } + ] +} + +function getTotalSize(node) { + if (node.type === "file") { + return node.size + } + + // Folder: sum sizes of all children + let total = 0 + for (const child of node.children) { + total += getTotalSize(child) + } + return total +} + +console.log(getTotalSize(fileSystem)) // 175 +``` + +--- + +## Recursion vs Iteration + +Every recursive solution can be rewritten using loops, and vice versa. Here's when to choose each: + +| Aspect | Recursion | Iteration (Loops) | +|--------|-----------|-------------------| +| **Readability** | Often cleaner for tree-like problems | Usually simpler for linear tasks | +| **Memory** | Uses call stack (one frame per call) | Uses fixed/minimal memory | +| **Performance** | Function call overhead | Generally faster | +| **Stack Risk** | Stack overflow possible (~10,000+ calls) | No stack overflow risk | +| **Best For** | Trees, graphs, nested structures | Simple counting, linear arrays | + +<Tabs> + <Tab title="Recursive"> + ```javascript + // Recursive factorial + function factorial(n) { + if (n <= 1) return 1 + return n * factorial(n - 1) + } + ``` + + **Pros:** Matches the mathematical definition exactly. Easy to read. + + **Cons:** Uses O(n) stack space. Could overflow for large n. + </Tab> + <Tab title="Iterative"> + ```javascript + // Iterative factorial + function factorial(n) { + let result = 1 + for (let i = 2; i <= n; i++) { + result *= i + } + return result + } + ``` + + **Pros:** Uses O(1) space. No stack overflow risk. Faster. + + **Cons:** Slightly less intuitive mapping to the math. + </Tab> +</Tabs> + +### When to Use Recursion + +- **Tree structures**: DOM traversal, file systems, org charts +- **Divide and conquer algorithms**: Merge sort, quick sort, binary search +- **Problems with self-similar subproblems**: Factorial, Fibonacci, fractals +- **When code clarity matters more than performance**: Prototyping, readable code + +### When to Use Iteration + +- **Simple loops**: Counting, summing arrays +- **Performance-critical code**: Tight loops in hot paths +- **Very deep structures**: Anything that might exceed ~10,000 levels +- **Memory-constrained environments**: Each recursive call uses stack space + +<Tip> +**Rule of thumb:** Start with whichever approach feels more natural for the problem. If you run into stack overflow issues or performance problems, consider converting to iteration. +</Tip> + +--- + +## Common Mistakes + +Here are the most frequent bugs when writing recursive functions: + +### Mistake #1: Missing or Incorrect Base Case + +Without a base case, the function calls itself forever until the stack overflows: + +```javascript +// ❌ WRONG - No base case! +function countdown(n) { + console.log(n) + countdown(n - 1) // Never stops! +} + +countdown(3) // 3, 2, 1, 0, -1, -2... CRASH! +// RangeError: Maximum call stack size exceeded +``` + +```javascript +// ✓ CORRECT - Has a base case +function countdown(n) { + if (n < 0) return // Base case: stop at negative + console.log(n) + countdown(n - 1) +} + +countdown(3) // 3, 2, 1, 0 (then stops) +``` + +<Warning> +**The error you'll see:** `RangeError: Maximum call stack size exceeded`. This means you've made too many recursive calls without returning. Check your base case! +</Warning> + +### Mistake #2: Base Case That's Never Reached + +Even with a base case, if your logic never reaches it, you'll still crash: + +```javascript +// ❌ WRONG - Base case can never be reached +function countdown(n) { + if (n === 0) return // Only stops at exactly 0 + console.log(n) + countdown(n - 2) // Skips over 0 when starting with odd number! +} + +countdown(5) // 5, 3, 1, -1, -3... CRASH! +``` + +```javascript +// ✓ CORRECT - Base case is reachable +function countdown(n) { + if (n <= 0) return // Stops at 0 or below + console.log(n) + countdown(n - 2) +} + +countdown(5) // 5, 3, 1 (then stops) +``` + +### Mistake #3: Forgetting to Return the Recursive Call + +If you call the function recursively but don't return its result, you lose the value: + +```javascript +// ❌ WRONG - Missing return +function sum(n) { + if (n === 1) return 1 + sum(n - 1) + n // Calculated but not returned! +} + +console.log(sum(5)) // undefined +``` + +```javascript +// ✓ CORRECT - Returns the result +function sum(n) { + if (n === 1) return 1 + return sum(n - 1) + n // Return the calculation +} + +console.log(sum(5)) // 15 +``` + +### Mistake #4: Modifying Shared State + +Be careful about variables outside the function that recursive calls might all modify: + +```javascript +// ❌ PROBLEMATIC - Shared mutable state +let count = 0 + +function countNodes(node) { + if (node === null) return + count++ // All calls modify the same variable + countNodes(node.left) + countNodes(node.right) +} +// If you call countNodes twice, count keeps increasing! +``` + +```javascript +// ✓ BETTER - Return values instead of mutating +function countNodes(node) { + if (node === null) return 0 + return 1 + countNodes(node.left) + countNodes(node.right) +} +// Each call is independent +``` + +### Mistake #5: Inefficient Overlapping Subproblems + +The naive Fibonacci implementation recalculates the same values many times: + +```javascript +// ❌ VERY SLOW - Exponential time complexity +function fib(n) { + if (n <= 1) return n + return fib(n - 1) + fib(n - 2) +} + +fib(40) // Takes several seconds! +fib(50) // Takes minutes or crashes +``` + +This is fixed with memoization, covered in the next section. + +--- + +## Optimizing Recursive Functions + +### Memoization + +**Memoization** means caching the results of function calls so you don't recompute the same thing twice. It's especially useful for recursive functions with overlapping subproblems. + +```javascript +// Fibonacci with memoization +function fibonacci(n, memo = {}) { + // Check if we already calculated this + if (n in memo) { + return memo[n] + } + + // Base cases + if (n <= 1) return n + + // Calculate and cache the result + memo[n] = fibonacci(n - 1, memo) + fibonacci(n - 2, memo) + return memo[n] +} + +console.log(fibonacci(50)) // 12586269025 (instant!) +console.log(fibonacci(100)) // 354224848179262000000 (still instant!) +``` + +The naive Fibonacci has O(2^n) time complexity. With memoization, it's O(n). That's the difference between billions of operations and just 100. + +### Tail Recursion + +A **tail recursive** function is one where the recursive call is the very last thing the function does. There's no computation after the call returns. + +```javascript +// NOT tail recursive - multiplication happens AFTER the recursive call +function factorial(n) { + if (n <= 1) return 1 + return n * factorial(n - 1) // Still need to multiply after call returns +} + +// Tail recursive version - uses an accumulator +function factorialTail(n, accumulator = 1) { + if (n <= 1) return accumulator + return factorialTail(n - 1, accumulator * n) // Nothing to do after this returns +} +``` + +**Why does this matter?** In theory, tail recursive functions can be optimized by the JavaScript engine to reuse the same stack frame, avoiding stack overflow entirely. This is called **Tail Call Optimization (TCO)**. + +<Note> +**Reality check:** Most JavaScript engines (V8 in Chrome/Node, SpiderMonkey in Firefox) **do not implement TCO**. Safari's JavaScriptCore is the notable exception. So in practice, tail recursion doesn't prevent stack overflow in most environments. Still, it's good to understand the concept, as it's important in functional programming languages like Haskell and Scheme. +</Note> + +### Converting to Iteration + +If you're hitting stack limits, consider converting your recursion to a loop with an explicit stack: + +```javascript +// Recursive tree traversal +function sumTreeRecursive(node) { + if (node === null) return 0 + return node.value + sumTreeRecursive(node.left) + sumTreeRecursive(node.right) +} + +// Iterative version using explicit stack +function sumTreeIterative(root) { + if (root === null) return 0 + + let sum = 0 + const stack = [root] + + while (stack.length > 0) { + const node = stack.pop() + sum += node.value + + if (node.right) stack.push(node.right) + if (node.left) stack.push(node.left) + } + + return sum +} +``` + +The iterative version uses heap memory (the array) instead of stack memory, so it can handle much deeper structures. + +--- + +## Key Takeaways + +<Info> +**The key things to remember:** + +1. **Recursion = a function calling itself** to solve smaller versions of the same problem + +2. **Every recursive function needs a base case** that stops the recursion without making another call + +3. **The recursive case** breaks the problem into a smaller piece and calls the function again + +4. **Recursion uses the call stack** — each call adds a new frame with its own local variables + +5. **The base case must be reachable** — if it's not, you'll get infinite recursion and a stack overflow + +6. **Recursion shines for tree-like structures**: DOM traversal, nested objects, file systems, linked lists + +7. **Loops are often better for simple iteration** — less overhead, no stack overflow risk + +8. **Watch for stack overflow** on deep recursion (most browsers limit to ~10,000 calls) + +9. **Memoization fixes inefficient recursion** by caching results of repeated subproblems + +10. **Recursion isn't JavaScript-specific** — it's a universal programming technique you'll use in any language +</Info> + +--- + +## Test Your Knowledge + +<AccordionGroup> + <Accordion title="What are the two essential parts of a recursive function?"> + **Answer:** + + 1. **Base case**: The condition that stops the recursion. It returns a value without making another recursive call. + + 2. **Recursive case**: The part where the function calls itself with a simpler or smaller version of the problem. + + ```javascript + function example(n) { + if (n === 0) return "done" // Base case + return example(n - 1) // Recursive case + } + ``` + </Accordion> + + <Accordion title="What happens if you forget the base case?"> + **Answer:** + + The function calls itself infinitely until JavaScript throws a `RangeError: Maximum call stack size exceeded`. This is called a **stack overflow** because each call adds a frame to the call stack until it runs out of memory. + + ```javascript + // This will crash + function broken(n) { + return broken(n - 1) // No base case to stop! + } + broken(5) // RangeError: Maximum call stack size exceeded + ``` + </Accordion> + + <Accordion title="Write a recursive function to find the length of an array without using .length"> + **Answer:** + + ```javascript + function arrayLength(arr) { + // Base case: empty array has length 0 + if (arr.length === 0) return 0 + + // Recursive case: 1 + length of the rest + return 1 + arrayLength(arr.slice(1)) + } + + console.log(arrayLength([1, 2, 3, 4])) // 4 + console.log(arrayLength([])) // 0 + ``` + + Note: We use `.length` only to check if the array is empty (our base case). The actual counting happens through recursion, not by directly returning `.length`. + </Accordion> + + <Accordion title="Why is naive Fibonacci recursion inefficient, and how would you fix it?"> + **Answer:** + + Naive Fibonacci recalculates the same values many times. For example, `fib(5)` calculates `fib(3)` twice, `fib(2)` three times, etc. This leads to exponential O(2^n) time complexity. + + **The fix: Memoization.** Cache results so each value is only calculated once: + + ```javascript + function fibonacci(n, memo = {}) { + if (n in memo) return memo[n] + if (n <= 1) return n + + memo[n] = fibonacci(n - 1, memo) + fibonacci(n - 2, memo) + return memo[n] + } + ``` + + This reduces the time complexity to O(n). + </Accordion> + + <Accordion title="When should you choose recursion over a loop?"> + **Answer:** + + **Choose recursion when:** + - Working with tree-like or nested structures (DOM, file systems, JSON) + - The problem naturally divides into self-similar subproblems + - Code clarity is more important than maximum performance + - Implementing divide-and-conquer algorithms + + **Choose loops when:** + - Iterating through flat, linear data + - Performance is critical + - You might recurse more than ~10,000 levels deep + - Memory is constrained + </Accordion> + + <Accordion title="How does recursion relate to the call stack?"> + **Answer:** + + Each recursive call creates a new **execution context** that gets pushed onto the call stack. The function waits for its recursive call to return, keeping its context on the stack. + + When the base case is reached, contexts start popping off the stack as return values bubble back up. This is why deep recursion can cause stack overflow — too many contexts waiting at once. + + ``` + sumTo(3) calls → sumTo(2) calls → sumTo(1) + + Stack: [sumTo(3), sumTo(2), sumTo(1)] + ↓ returns 1 + Stack: [sumTo(3), sumTo(2)] + ↓ returns 3 + Stack: [sumTo(3)] + ↓ returns 6 + Stack: [] + ``` + </Accordion> +</AccordionGroup> + +--- + +## Related Concepts + +<CardGroup cols={2}> + <Card title="Call Stack" icon="layer-group" href="/concepts/call-stack"> + How JavaScript tracks function execution — the foundation of how recursion works under the hood. + </Card> + <Card title="Higher-Order Functions" icon="function" href="/concepts/higher-order-functions"> + Functions that take or return other functions. Many recursive patterns combine with higher-order functions. + </Card> + <Card title="Data Structures" icon="sitemap" href="/concepts/data-structures"> + Trees, linked lists, and graphs — data structures that are naturally recursive. + </Card> + <Card title="Pure Functions" icon="sparkles" href="/concepts/pure-functions"> + Functions with no side effects. Recursive functions work best when they're pure. + </Card> +</CardGroup> + +--- + +## Reference + +<CardGroup cols={2}> + <Card title="Recursion — MDN Glossary" icon="book" href="https://developer.mozilla.org/en-US/docs/Glossary/Recursion"> + Official MDN definition with common examples including factorial, Fibonacci, and reduce. + </Card> + <Card title="Functions Guide: Recursion — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Functions#recursion"> + MDN's guide on recursive functions in JavaScript with DOM traversal examples. + </Card> +</CardGroup> + +## Articles + +<CardGroup cols={2}> + <Card title="Recursion and Stack" icon="newspaper" href="https://javascript.info/recursion"> + The definitive JavaScript recursion tutorial. Covers execution context, linked lists, and recursive data structures with interactive examples. + </Card> + <Card title="What is Recursion? A Recursive Function Explained" icon="newspaper" href="https://www.freecodecamp.org/news/what-is-recursion-in-javascript/"> + Beginner-friendly introduction with step-by-step breakdowns. Great for understanding the "why" behind recursion. + </Card> +</CardGroup> + +<CardGroup cols={2}> + <Card title="Recursion Explained (with Examples)" icon="newspaper" href="https://dev.to/christinamcmahon/recursion-explained-with-examples-4k1m"> + Visual explanation of factorial and Fibonacci with tree diagrams. Includes memoization introduction. + </Card> + <Card title="JavaScript Recursive Function" icon="newspaper" href="https://www.javascripttutorial.net/javascript-recursive-function/"> + Clear tutorial covering recursive function basics, countdowns, and sum calculations with detailed step-by-step explanations. + </Card> +</CardGroup> + +## Videos + +<CardGroup cols={2}> + <Card title="What Is Recursion - In Depth" icon="video" href="https://www.youtube.com/watch?v=6oDQaB2one8"> + Web Dev Simplified breaks down recursion with clear visuals and practical examples. Great for visual learners. + </Card> + <Card title="Recursion" icon="video" href="https://www.youtube.com/watch?v=k7-N8R0-KY4"> + Fun Fun Function's engaging explanation of recursion with personality and deeper conceptual insights. + </Card> +</CardGroup> + +<CardGroup cols={2}> + <Card title="Recursion Crash Course" icon="video" href="https://www.youtube.com/watch?v=lMBVwYrmFZQ"> + Colt Steele's practical crash course on recursion, perfect for interview preparation. + </Card> + <Card title="What on Earth is Recursion?" icon="video" href="https://www.youtube.com/watch?v=Mv9NEXX1VHc"> + Computerphile explains recursion from a computer science perspective with great conceptual depth. + </Card> +</CardGroup> diff --git a/docs/concepts/regular-expressions.mdx b/docs/concepts/regular-expressions.mdx new file mode 100644 index 00000000..befec72c --- /dev/null +++ b/docs/concepts/regular-expressions.mdx @@ -0,0 +1,661 @@ +--- +title: "Regular Expressions: Pattern Matching in JavaScript" +sidebarTitle: "Regular Expressions: Pattern Matching" +description: "Learn regular expressions in JavaScript. Covers pattern syntax, character classes, quantifiers, flags, capturing groups, and methods like test, match, and replace." +--- + +How do you check if an email address is valid? How do you find and replace all phone numbers in a document? How can you extract hashtags from a tweet? + +```javascript +// Check if a string contains only digits +const isAllDigits = /^\d+$/.test('12345') +console.log(isAllDigits) // true + +// Find all words starting with capital letters +const text = 'Hello World from JavaScript' +const capitalWords = text.match(/\b[A-Z][a-z]*\b/g) +console.log(capitalWords) // ["Hello", "World"] +``` + +The answer is **[regular expressions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions)** (often called "regex" or "regexp"). They're patterns that describe what you're looking for in text, and JavaScript has powerful built-in support for them. + +<Info> +**What you'll learn in this guide:** +- Creating regex with literals (`/pattern/`) and the `RegExp` constructor +- Character classes, quantifiers, and anchors +- Key methods: `test()`, `match()`, `replace()`, `split()` +- Capturing groups for extracting parts of matches +- Flags that change how patterns match +- Common real-world patterns (email, phone, URL) +</Info> + +<Warning> +**Prerequisite:** This guide assumes you're comfortable with [strings](/concepts/primitive-types) in JavaScript. You don't need any prior regex experience — we'll start from the basics. +</Warning> + +--- + +## What Are Regular Expressions? + +A **[regular expression](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp)** is a pattern used to match character combinations in strings. In JavaScript, regex are objects that you can use with string methods to search, validate, extract, and replace text. They use a special syntax where characters like `\d`, `*`, and `^` have special meanings beyond their literal values. + +### Two Ways to Create Regex + +```javascript +// 1. Literal syntax (preferred for static patterns) +const pattern1 = /hello/ + +// 2. Constructor syntax (useful for dynamic patterns) +const pattern2 = new RegExp('hello') + +// Both work the same way +console.log(pattern1.test('hello world')) // true +console.log(pattern2.test('hello world')) // true +``` + +Use the literal syntax when you know the pattern ahead of time. Use the constructor when you need to build patterns dynamically, like from user input: + +```javascript +function findWord(text, word) { + const pattern = new RegExp(word, 'gi') // case-insensitive, global + return text.match(pattern) +} + +console.log(findWord('Hello hello HELLO', 'hello')) // ["Hello", "hello", "HELLO"] +``` + +--- + +## The Detective Analogy + +Think of regex like giving a detective a description to find suspects in a crowd: + +- **Literal characters** (`abc`) — "Find someone named 'abc'" +- **Character classes** (`[aeiou]`) — "Find someone with a vowel in their name" +- **Quantifiers** (`a+`) — "Find someone with one or more 'a's in their name" +- **Anchors** (`^`, `$`) — "They must be at the start/end of the line" + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ REGEX PATTERN MATCHING │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Pattern: /\d{3}-\d{4}/ String: "Call 555-1234 today" │ +│ │ +│ Step 1: Find 3 digits (\d{3}) → "555" ✓ │ +│ Step 2: Find a hyphen (-) → "-" ✓ │ +│ Step 3: Find 4 digits (\d{4}) → "1234" ✓ │ +│ │ +│ Result: Match found! → "555-1234" │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Building Blocks: Character Classes + +Character classes let you match *types* of characters rather than specific ones. + +| Pattern | Matches | Example | +|---------|---------|---------| +| `.` | Any character except newline | `/a.c/` matches "abc", "a1c", "a-c" | +| `\d` | Any digit [0-9] | `/\d{3}/` matches "123" | +| `\D` | Any non-digit | `/\D+/` matches "abc" | +| `\w` | Word character [A-Za-z0-9_] | `/\w+/` matches "hello_123" | +| `\W` | Non-word character | `/\W/` matches "!" or " " | +| `\s` | Whitespace (space, tab, newline) | `/\s+/` matches " " | +| `\S` | Non-whitespace | `/\S+/` matches "hello" | +| `[abc]` | Any of a, b, or c | `/[aeiou]/` matches any vowel | +| `[^abc]` | Not a, b, or c | `/[^0-9]/` matches non-digits | +| `[a-z]` | Character range | `/[A-Za-z]/` matches any letter | + +```javascript +// Match a phone number pattern: 3 digits, hyphen, 4 digits +const phone = /\d{3}-\d{4}/ +console.log(phone.test('555-1234')) // true +console.log(phone.test('55-1234')) // false + +// Match words (letters, digits, underscores) +const words = 'hello_world 123 test!' +console.log(words.match(/\w+/g)) // ["hello_world", "123", "test"] +``` + +--- + +## Building Blocks: Quantifiers + +Quantifiers specify how many times a pattern should repeat. + +| Quantifier | Meaning | Example | +|------------|---------|---------| +| `*` | 0 or more | `/ab*c/` matches "ac", "abc", "abbbbc" | +| `+` | 1 or more | `/ab+c/` matches "abc", "abbbbc" (not "ac") | +| `?` | 0 or 1 (optional) | `/colou?r/` matches "color", "colour" | +| `{n}` | Exactly n times | `/\d{4}/` matches "2024" | +| `{n,}` | n or more times | `/\d{2,}/` matches "12", "123", "1234" | +| `{n,m}` | Between n and m times | `/\d{2,4}/` matches "12", "123", "1234" | + +```javascript +// Match optional 's' for plural +const plural = /apple(s)?/ +console.log(plural.test('apple')) // true +console.log(plural.test('apples')) // true + +// Match 1 or more digits +const numbers = 'I have 42 apples and 7 oranges' +console.log(numbers.match(/\d+/g)) // ["42", "7"] +``` + +--- + +## Building Blocks: Anchors + +Anchors match *positions* in the string, not characters. + +| Anchor | Position | +|--------|----------| +| `^` | Start of string (or line with `m` flag) | +| `$` | End of string (or line with `m` flag) | +| `\b` | Word boundary | +| `\B` | Not a word boundary | + +```javascript +// Must start with "Hello" +console.log(/^Hello/.test('Hello World')) // true +console.log(/^Hello/.test('Say Hello')) // false + +// Must end with a digit +console.log(/\d$/.test('Room 42')) // true +console.log(/\d$/.test('42 rooms')) // false + +// Word boundaries prevent partial matches +console.log(/\bcat\b/.test('cat')) // true +console.log(/\bcat\b/.test('category')) // false (cat is part of a larger word) +``` + +--- + +## Methods for Using Regex + +JavaScript provides several methods for working with regular expressions: + +| Method | Returns | Use Case | +|--------|---------|----------| +| `regex.test(str)` | `true` or `false` | Simple validation | +| `str.match(regex)` | Array or `null` | Find matches | +| `str.matchAll(regex)` | Iterator | Find all matches with details | +| `str.search(regex)` | Index or `-1` | Find position of first match | +| `str.replace(regex, replacement)` | New string | Replace matches | +| `str.split(regex)` | Array | Split by pattern | +| `regex.exec(str)` | Match array or `null` | Detailed match info (stateful) | + +### test() — Simple Validation + +```javascript +const emailPattern = /\S+@\S+\.\S+/ + +console.log(emailPattern.test('user@example.com')) // true +console.log(emailPattern.test('invalid-email')) // false +``` + +### match() — Find Matches + +```javascript +const text = 'My numbers: 123, 456, 789' + +// Without 'g' flag: returns first match with details +console.log(text.match(/\d+/)) +// ["123", index: 12, input: "My numbers: 123, 456, 789"] + +// With 'g' flag: returns all matches +console.log(text.match(/\d+/g)) +// ["123", "456", "789"] +``` + +### matchAll() — All Matches with Details + +When you need all matches AND details (like captured groups), use `matchAll()`. It requires the `g` flag and returns an iterator: + +```javascript +const text = 'Call 555-1234 or 555-5678' +const pattern = /(\d{3})-(\d{4})/g + +for (const match of text.matchAll(pattern)) { + console.log(`Found: ${match[0]}, Prefix: ${match[1]}, Number: ${match[2]}`) +} +// "Found: 555-1234, Prefix: 555, Number: 1234" +// "Found: 555-5678, Prefix: 555, Number: 5678" +``` + +### search() — Find Position + +```javascript +const text = 'Hello World' +console.log(text.search(/World/)) // 6 (index where match starts) +console.log(text.search(/xyz/)) // -1 (not found) +``` + +### replace() — Replace Matches + +```javascript +// Replace first occurrence +console.log('hello world'.replace(/o/, '0')) +// "hell0 world" + +// Replace all occurrences (with 'g' flag) +console.log('hello world'.replace(/o/g, '0')) +// "hell0 w0rld" + +// Use captured groups in replacement +console.log('John Smith'.replace(/(\w+) (\w+)/, '$2, $1')) +// "Smith, John" +``` + +### split() — Split by Pattern + +```javascript +// Split on one or more whitespace characters +const words = 'hello world foo'.split(/\s+/) +console.log(words) // ["hello", "world", "foo"] + +// Split on commas with optional spaces +const items = 'a, b,c , d'.split(/\s*,\s*/) +console.log(items) // ["a", "b", "c", "d"] +``` + +### exec() — Detailed Match Info + +`exec()` is similar to `match()` but is called on the regex. With the `g` flag, calling it repeatedly finds the next match each time: + +```javascript +const pattern = /\d+/g +const text = 'a1b22c333' + +console.log(pattern.exec(text)) // ["1", index: 1] +console.log(pattern.exec(text)) // ["22", index: 3] +console.log(pattern.exec(text)) // ["333", index: 6] +console.log(pattern.exec(text)) // null (no more matches) +``` + +--- + +## Flags + +Flags modify how the pattern matches. Add them after the closing slash. + +| Flag | Name | Effect | +|------|------|--------| +| `g` | Global | Find all matches, not just the first | +| `i` | Case-insensitive | `a` matches `A` | +| `m` | Multiline | `^` and `$` match at each line's start/end | +| `s` | DotAll | `.` matches newlines too | + +```javascript +// Case-insensitive matching +console.log(/hello/i.test('HELLO')) // true + +// Global: find all matches +console.log('abcabc'.match(/a/g)) // ["a", "a"] +console.log('abcabc'.match(/a/)) // ["a", index: 0, input: "abcabc", ...] (first match with details) + +// Multiline: ^ and $ match each line +const multiline = 'line1\nline2\nline3' +console.log(multiline.match(/^line\d/gm)) // ["line1", "line2", "line3"] +``` + +--- + +## Capturing Groups + +Parentheses `()` create **capturing groups** that let you extract parts of a match. + +```javascript +// Extract area code and number separately +const phonePattern = /\((\d{3})\) (\d{3}-\d{4})/ +const match = '(555) 123-4567'.match(phonePattern) + +console.log(match[0]) // "(555) 123-4567" (full match) +console.log(match[1]) // "555" (first group) +console.log(match[2]) // "123-4567" (second group) +``` + +### Named Groups + +Use `(?<name>pattern)` to give groups meaningful names: + +```javascript +const datePattern = /(?<month>\d{2})-(?<day>\d{2})-(?<year>\d{4})/ +const match = '12-25-2024'.match(datePattern) + +console.log(match.groups.month) // "12" +console.log(match.groups.day) // "25" +console.log(match.groups.year) // "2024" +``` + +### Using Groups in Replace + +Reference captured groups with `$1`, `$2`, etc. (or `$<name>` for named groups): + +```javascript +// Reformat date from MM-DD-YYYY to YYYY/MM/DD +const date = '12-25-2024' +const reformatted = date.replace( + /(\d{2})-(\d{2})-(\d{4})/, + '$3/$1/$2' +) +console.log(reformatted) // "2024/12/25" +``` + +--- + +## The #1 Regex Mistake: Greedy vs Lazy + +By default, quantifiers are **greedy**. They match as much as possible. Add `?` to make them **lazy** (match as little as possible). + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ GREEDY VS LAZY │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ String: "<div>Hello</div><div>World</div>" │ +│ │ +│ GREEDY: /<div>.*<\/div>/ LAZY: /<div>.*?<\/div>/ │ +│ Matches: "<div>Hello</div> Matches: "<div>Hello</div>" │ +│ <div>World</div>" │ +│ (Everything from first (Just the first div) │ +│ <div> to LAST </div>) │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +```javascript +const html = '<div>Hello</div><div>World</div>' + +// Greedy: matches everything between first <div> and LAST </div> +console.log(html.match(/<div>.*<\/div>/)[0]) +// "<div>Hello</div><div>World</div>" + +// Lazy: stops at first </div> +console.log(html.match(/<div>.*?<\/div>/)[0]) +// "<div>Hello</div>" +``` + +<Tip> +**Rule of Thumb:** When matching content between delimiters (like HTML tags, quotes, or brackets), prefer lazy quantifiers (`*?`, `+?`) to avoid matching too much. +</Tip> + +--- + +## Common Patterns + +Here are some practical patterns you can use in your projects: + +```javascript +// Email (basic validation) +const email = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ +console.log(email.test('user@example.com')) // true + +// URL +const url = /^https?:\/\/[^\s]+$/ +console.log(url.test('https://example.com/path')) // true + +// Phone (US format: 123-456-7890 or (123) 456-7890) +const phone = /^(\(\d{3}\)|\d{3})[-.\s]?\d{3}[-.\s]?\d{4}$/ +console.log(phone.test('(555) 123-4567')) // true +console.log(phone.test('555-123-4567')) // true + +// Username (alphanumeric, 3-16 chars) +const username = /^[a-zA-Z0-9_]{3,16}$/ +console.log(username.test('john_doe123')) // true +``` + +<Warning> +**Don't go overboard.** Regex is great for pattern matching, but it's not always the best tool. For complex validation like email addresses (which have a surprisingly complex spec), consider using a dedicated validation library. The email regex above works for most cases but won't catch every edge case. +</Warning> + +--- + +## Key Takeaways + +<Info> +**The key things to remember:** + +1. **Regex = patterns for strings** — They describe what you're looking for, not literal text + +2. **Two ways to create** — `/pattern/` literals or `new RegExp('pattern')` + +3. **Character classes** — `\d` (digits), `\w` (word chars), `\s` (whitespace), `.` (any) + +4. **Quantifiers** — `*` (0+), `+` (1+), `?` (0-1), `{n,m}` (specific range) + +5. **Anchors** — `^` (start), `$` (end), `\b` (word boundary) + +6. **test() for validation** — Returns true/false + +7. **match() for extraction** — Returns matches or null + +8. **Flags change behavior** — `g` (global), `i` (case-insensitive), `m` (multiline) + +9. **Groups capture parts** — Use `()` to extract portions of matches + +10. **Greedy vs lazy** — Add `?` after quantifiers to match minimally +</Info> + +--- + +## Test Your Knowledge + +<AccordionGroup> + <Accordion title="Question 1: What's the difference between /pattern/ and new RegExp('pattern')?"> + **Answer:** + + Both create a regex object, but they differ in when to use them: + + - **Literal `/pattern/`** — Use for static patterns known at write time. The pattern is compiled when the script loads. + - **`new RegExp('pattern')`** — Use for dynamic patterns built at runtime (e.g., from user input). Remember to escape backslashes: `new RegExp('\\d+')`. + + ```javascript + // Static pattern - use literal + const digits = /\d+/ + + // Dynamic pattern - use constructor + const searchTerm = 'hello' + const dynamic = new RegExp(searchTerm, 'gi') + ``` + </Accordion> + + <Accordion title="Question 2: What does \b match?"> + **Answer:** + + `\b` matches a **word boundary** — the position between a word character (`\w`) and a non-word character. It doesn't match any actual character; it matches a position. + + ```javascript + // \b prevents partial matches + console.log(/\bcat\b/.test('cat')) // true + console.log(/\bcat\b/.test('category')) // false + console.log(/\bcat\b/.test('the cat')) // true + ``` + + Word boundaries are useful when you want to match whole words only. + </Accordion> + + <Accordion title="Question 3: How do you make a quantifier lazy?"> + **Answer:** + + Add a `?` after the quantifier to make it lazy (non-greedy): + + - `*?` — Match 0 or more, as few as possible + - `+?` — Match 1 or more, as few as possible + - `??` — Match 0 or 1, preferring 0 + - `{n,m}?` — Match between n and m, as few as possible + + ```javascript + const text = '<b>bold</b> and <b>more bold</b>' + + // Greedy: matches everything between first <b> and last </b> + text.match(/<b>.*<\/b>/)[0] // "<b>bold</b> and <b>more bold</b>" + + // Lazy: matches just the first <b>...</b> + text.match(/<b>.*?<\/b>/)[0] // "<b>bold</b>" + ``` + </Accordion> + + <Accordion title="Question 4: What's the difference between match() with and without the g flag?"> + **Answer:** + + - **Without `g`**: Returns first match with full details (captured groups, index, input) + - **With `g`**: Returns array of all matches (just the matched strings, no details) + + ```javascript + const text = 'cat and cat' + + // Without g: detailed info about first match + text.match(/cat/) + // ["cat", index: 0, input: "cat and cat"] + + // With g: all matches, no details + text.match(/cat/g) + // ["cat", "cat"] + ``` + + Use `matchAll()` if you need both all matches AND details for each. + </Accordion> + + <Accordion title="Question 5: How do you reference a captured group in a replacement string?"> + **Answer:** + + Use `$1`, `$2`, etc. for numbered groups, or `$<name>` for named groups: + + ```javascript + // Numbered groups + 'John Smith'.replace(/(\w+) (\w+)/, '$2, $1') + // "Smith, John" + + // Named groups + '2024-12-25'.replace( + /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/, + '$<month>/$<day>/$<year>' + ) + // "12/25/2024" + + // $& references the entire match + 'hello'.replace(/\w+/, '[$&]') + // "[hello]" + ``` + </Accordion> + + <Accordion title="Question 6: How do you match special regex characters literally?"> + **Answer:** + + Escape special characters with a backslash `\`. Characters that need escaping: `. * + ? ^ $ { } [ ] \ | ( )` and `/` in literal syntax + + ```javascript + // Match a literal period + /\./.test('file.txt') // true + /\./.test('filetxt') // false + + // Match a literal dollar sign + /\$\d+/.test('$100') // true + + // When using RegExp constructor, double-escape + new RegExp('\\d+\\.\\d+') // matches "3.14" + ``` + + For dynamic patterns from user input, escape all special chars: + + ```javascript + function escapeRegex(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + } + + const userInput = 'hello.world' + const pattern = new RegExp(escapeRegex(userInput)) + pattern.test('hello.world') // true + pattern.test('helloXworld') // false + ``` + </Accordion> +</AccordionGroup> + +--- + +## Related Concepts + +<CardGroup cols={2}> + <Card title="Primitive Types" icon="cube" href="/concepts/primitive-types"> + Strings are one of JavaScript's primitive types + </Card> + <Card title="Map, Reduce, Filter" icon="filter" href="/concepts/map-reduce-filter"> + Process arrays of matches from regex operations + </Card> + <Card title="Error Handling" icon="triangle-exclamation" href="/concepts/error-handling"> + Invalid regex patterns throw SyntaxError + </Card> + <Card title="Clean Code" icon="broom" href="/concepts/clean-code"> + Write maintainable regex with comments and named groups + </Card> +</CardGroup> + +--- + +## Reference + +<CardGroup cols={2}> + <Card title="Regular Expressions — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions"> + Comprehensive MDN guide covering all regex syntax and features + </Card> + <Card title="RegExp Object — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp"> + Reference for the RegExp constructor, methods, and properties + </Card> + <Card title="String.prototype.match() — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match"> + Documentation for the match() method + </Card> + <Card title="String.prototype.replace() — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace"> + Documentation for the replace() method + </Card> +</CardGroup> + +## Articles + +<CardGroup cols={2}> + <Card title="Regular Expressions — JavaScript.info" icon="newspaper" href="https://javascript.info/regular-expressions"> + Multi-chapter deep dive covering every regex feature with interactive examples. The go-to tutorial for learning regex thoroughly. + </Card> + <Card title="Learn Regex the Easy Way" icon="newspaper" href="https://github.com/ziishaned/learn-regex"> + Visual cheatsheet with clear examples for each pattern type. Great reference when you forget specific syntax. 46k+ GitHub stars. + </Card> + <Card title="Regular Expressions — Eloquent JavaScript" icon="newspaper" href="https://eloquentjavascript.net/09_regexp.html"> + Chapter from the classic free JavaScript book. Explains the theory and mechanics behind regex with elegant examples. + </Card> + <Card title="A Practical Guide to Regular Expressions" icon="newspaper" href="https://www.freecodecamp.org/news/practical-regex-guide-with-real-life-examples/"> + Hands-on freeCodeCamp guide focused on real-world use cases like log parsing, file renaming, and form validation. + </Card> +</CardGroup> + +## Videos + +<CardGroup cols={2}> + <Card title="Learn Regular Expressions In 20 Minutes" icon="video" href="https://www.youtube.com/watch?v=rhzKDrUiJVk"> + Web Dev Simplified covers all the essentials without filler. Great if you want to learn regex quickly and start using it. + </Card> + <Card title="Regular Expressions (Regex) in JavaScript" icon="video" href="https://www.youtube.com/watch?v=909NfO1St0A"> + Fireship's fast-paced 100 seconds style overview. Perfect for a quick refresher or introduction to what regex can do. + </Card> + <Card title="JavaScript Regex — Programming with Mosh" icon="video" href="https://www.youtube.com/watch?v=VrT3TRDDE4M"> + Mosh Hamedani's beginner-friendly walkthrough with practical JavaScript examples you can follow along with. + </Card> +</CardGroup> + +## Tools + +<CardGroup cols={2}> + <Card title="regex101" icon="flask" href="https://regex101.com/"> + Interactive regex tester with real-time explanation of your pattern. Shows match groups, explains each part, and lets you test against sample text. + </Card> + <Card title="RegExr" icon="wand-magic-sparkles" href="https://regexr.com/"> + Visual regex editor with community patterns and a helpful cheatsheet sidebar. Great for learning and building patterns. + </Card> + <Card title="Regexlearn" icon="graduation-cap" href="https://regexlearn.com/"> + Interactive step-by-step tutorial that teaches regex through practice. Gamified learning with progressive difficulty. + </Card> +</CardGroup> diff --git a/docs/concepts/scope-and-closures.mdx b/docs/concepts/scope-and-closures.mdx new file mode 100644 index 00000000..6b584f7c --- /dev/null +++ b/docs/concepts/scope-and-closures.mdx @@ -0,0 +1,1186 @@ +--- +title: "Scope and Closures: How Variables Really Work in JavaScript" +sidebarTitle: "Scope and Closures: How Variables Really Work" +description: "Learn JavaScript scope and closures. Understand the three types of scope, var vs let vs const, lexical scoping, the scope chain, and closure patterns for data privacy." +--- + +Why can some variables be accessed from anywhere in your code, while others seem to disappear? How do functions "remember" variables from their parent functions, even after those functions have finished running? + +```javascript +function createCounter() { + let count = 0 // This variable is "enclosed" + + return function() { + count++ + return count + } +} + +const counter = createCounter() +console.log(counter()) // 1 +console.log(counter()) // 2 — it remembers! +``` + +The answers lie in understanding **scope** and **closures**. These two fundamental concepts govern how variables work in JavaScript. Scope determines *where* variables are visible, while closures allow functions to *remember* their original environment. + +<Info> +**What you'll learn in this guide:** +- The 3 types of scope: global, function, and block +- How [`var`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/var), [`let`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let), and [`const`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const) behave differently +- What lexical scope means and how the scope chain works +- What closures are and why every JavaScript developer must understand them +- Practical patterns: data privacy, factories, and memoization +- The classic closure gotchas and how to avoid them +</Info> + +<Warning> +**Prerequisite:** This guide builds on your understanding of the [call stack](/concepts/call-stack). Knowing how JavaScript tracks function execution will help you understand how scope and closures work under the hood. +</Warning> + +--- + +## What is Scope in JavaScript? + +**[Scope](https://developer.mozilla.org/en-US/docs/Glossary/Scope)** is the current context of execution in which values and expressions are "visible" or can be referenced. It's the set of rules that determines where and how variables can be accessed in your code. If a variable is not in the current scope, it cannot be used. Scopes can be nested, and inner scopes have access to outer scopes, but not vice versa. + +--- + +## The Office Building Analogy + +Imagine it's after hours and you're wandering through your office building (legally, you work there, promise). You notice something interesting about what you can and can't see: + +- **Inside your private office**, you can see everything on your desk, peek into the hallway through your door, and even see the lobby through the glass walls +- **In the hallway**, you can see the lobby clearly, but those private offices? Their blinds are shut. No peeking allowed +- **In the lobby**, you're limited to just what's there: the reception desk, some chairs, maybe a sad-looking plant + +``` +┌─────────────────────────────────────────────────────────────┐ +│ LOBBY (Global Scope) │ +│ reception = "Welcome Desk" │ +│ │ +│ ┌───────────────────────────────────────────────────┐ │ +│ │ HALLWAY (Function Scope) │ │ +│ │ hallwayPlant = "Fern" │ │ +│ │ │ │ +│ │ ┌───────────────────────────────────────┐ │ │ +│ │ │ PRIVATE OFFICE (Block Scope) │ │ │ +│ │ │ secretDocs = "Confidential" │ │ │ +│ │ │ │ │ │ +│ │ │ Can see: secretDocs ✓ │ │ │ +│ │ │ Can see: hallwayPlant ✓ │ │ │ +│ │ │ Can see: reception ✓ │ │ │ +│ │ └───────────────────────────────────────┘ │ │ +│ │ │ │ +│ │ Cannot see: secretDocs ✗ │ │ +│ └───────────────────────────────────────────────────┘ │ +│ │ +│ Cannot see: hallwayPlant, secretDocs ✗ │ +└─────────────────────────────────────────────────────────────┘ +``` + +This is exactly how **scope** works in JavaScript! Code in inner scopes can "look out" and access variables from outer scopes, but outer scopes can never "look in" to inner scopes. + +And here's where it gets really interesting: imagine someone who worked in that private office quits and leaves the building. But they took a mental snapshot of everything in there: the passwords on sticky notes, the secret project plans, the snack drawer location. Even though they've left, they still *remember* everything. That's essentially what a **closure** is: a function that "remembers" the scope where it was created, even after that scope is gone. + +### Why Does Scope Exist? + +Scope exists for three critical reasons: + +<AccordionGroup> + <Accordion title="1. Preventing Naming Conflicts"> + Without scope, every variable would be global. Imagine the chaos if every `i` in every `for` loop had to have a unique name! + + ```javascript + function countApples() { + let count = 0; // This 'count' is separate... + // ... + } + + function countOranges() { + let count = 0; // ...from this 'count' + // ... + } + ``` + </Accordion> + + <Accordion title="2. Memory Management"> + When a scope ends, variables declared in that scope can be [garbage collected](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_management#garbage_collection) (cleaned up from memory). This keeps your program efficient. + + ```javascript + function processData() { + let hugeArray = new Array(1000000); // Takes memory + // ... process it + } // hugeArray can now be garbage collected + ``` + </Accordion> + + <Accordion title="3. Encapsulation & Security"> + Scope allows you to hide implementation details and protect data from being accidentally modified. + + ```javascript + function createBankAccount() { + let balance = 0; // Private! Can't be accessed directly + + return { + deposit(amount) { balance += amount; }, + getBalance() { return balance; } + }; + } + ``` + </Accordion> +</AccordionGroup> + +--- + +## The Three Types of Scope + +JavaScript has three main types of scope. Understanding each one is fundamental to writing predictable code. + +<Note> +ES6 modules also introduce **module scope**, where top-level variables are scoped to the module rather than being global. Learn more in our [IIFE, Modules and Namespaces](/concepts/iife-modules) guide. +</Note> + +### 1. Global Scope + +Variables declared outside of any function or block are in the **global scope**. They're accessible from anywhere in your code. + +```javascript +// Global scope +const appName = "MyApp"; +let userCount = 0; + +function greet() { + console.log(appName); // ✓ Can access global variable + userCount++; // ✓ Can modify global variable +} + +if (true) { + console.log(appName); // ✓ Can access global variable +} +``` + +#### The Global Object + +In browsers, global variables become properties of the [`window`](https://developer.mozilla.org/en-US/docs/Web/API/Window) object. In Node.js, they attach to `global`. The modern, universal way to access the global object is [`globalThis`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis). + +```javascript +var oldSchool = "I'm on window"; // window.oldSchool (var only) +let modern = "I'm NOT on window"; // NOT on window + +console.log(window.oldSchool); // "I'm on window" +console.log(window.modern); // undefined +console.log(globalThis); // Works everywhere +``` + +<Warning> +**Avoid Global Pollution!** Too many global variables lead to naming conflicts, hard-to-track bugs, and code that's difficult to maintain. Keep your global scope clean. + +```javascript +// Bad: Polluting global scope +var userData = {}; +var settings = {}; +var helpers = {}; + +// Good: Use a single namespace +const MyApp = { + userData: {}, + settings: {}, + helpers: {} +}; +``` +</Warning> + +--- + +### 2. Function Scope + +Variables declared with [`var`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/var) inside a function are **function-scoped**. They're only accessible within that function. + +```javascript +function calculateTotal() { + var subtotal = 100; + var tax = 10; + var total = subtotal + tax; + + console.log(total); // ✓ 110 +} + +calculateTotal(); +// console.log(subtotal); // ✗ ReferenceError: subtotal is not defined +``` + +#### var Hoisting + +Variables declared with `var` are "hoisted" to the top of their function. This means JavaScript knows about them before the code runs, but they're initialized as `undefined` until the actual declaration line. + +```javascript +function example() { + console.log(message); // undefined (not an error!) + var message = "Hello"; + console.log(message); // "Hello" +} + +// JavaScript interprets this as: +function exampleHoisted() { + var message; // Declaration hoisted to top + console.log(message); // undefined + message = "Hello"; // Assignment stays in place + console.log(message); // "Hello" +} +``` + +<Tip> +**Hoisting Visualization:** + +``` +Your code: How JS sees it: +┌─────────────────────┐ ┌─────────────────────┐ +│ function foo() { │ │ function foo() { │ +│ │ │ var x; // hoisted│ +│ console.log(x); │ ──► │ console.log(x); │ +│ var x = 5; │ │ x = 5; │ +│ } │ │ } │ +└─────────────────────┘ └─────────────────────┘ +``` +</Tip> + +--- + +### 3. Block Scope + +Variables declared with [`let`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let) and [`const`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const) are **block-scoped**. A block is any code within curly braces `{}`: if statements, for loops, while loops, or just standalone blocks. + +```javascript +if (true) { + let blockLet = "I'm block-scoped"; + const blockConst = "Me too"; + var functionVar = "I escape the block!"; +} + +// console.log(blockLet); // ✗ ReferenceError +// console.log(blockConst); // ✗ ReferenceError +console.log(functionVar); // ✓ "I escape the block!" +``` + +#### The Temporal Dead Zone (TDZ) + +Unlike `var`, variables declared with `let` and `const` are not initialized until their declaration is evaluated. Accessing them before declaration causes a [`ReferenceError`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ReferenceError). This period is called the **Temporal Dead Zone**. + +```javascript +function demo() { + // TDZ for 'name' starts here + + console.log(name); // ReferenceError: Cannot access 'name' before initialization + + let name = "Alice"; // TDZ ends here + + console.log(name); // "Alice" +} +``` + +``` +┌────────────────────────────────────────────────────────────┐ +│ │ +│ function demo() { │ +│ │ +│ ┌────────────────────────────────────────────────┐ │ +│ │ TEMPORAL DEAD ZONE │ │ +│ │ │ │ +│ │ 'name' exists but cannot be accessed yet! │ │ +│ │ │ │ +│ │ console.log(name); // ReferenceError │ │ +│ │ │ │ +│ └────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ let name = "Alice"; // TDZ ends here │ +│ │ +│ console.log(name); // "Alice" - works fine! │ +│ │ +│ } │ +│ │ +└────────────────────────────────────────────────────────────┘ +``` + +<Note> +The TDZ exists to catch programming errors. It's actually a good thing! It prevents you from accidentally using variables before they're ready. +</Note> + +--- + +## var vs let vs const + +Here's a comprehensive comparison of the three variable declaration keywords: + +| Feature | `var` | `let` | `const` | +|---------|-------|-------|---------| +| **Scope** | Function | Block | Block | +| **Hoisting** | Yes (initialized as `undefined`) | Yes (but TDZ) | Yes (but TDZ) | +| **Redeclaration** | ✓ Allowed | ✗ Error | ✗ Error | +| **Reassignment** | ✓ Allowed | ✓ Allowed | ✗ Error | +| **Must Initialize** | No | No | Yes | + +<Tabs> + <Tab title="Redeclaration"> + ```javascript + // var allows redeclaration (can cause bugs!) + var name = "Alice"; + var name = "Bob"; // No error, silently overwrites + console.log(name); // "Bob" + + // let and const prevent redeclaration + let age = 25 + // let age = 30 // SyntaxError: 'age' has already been declared + + const PI = 3.14 + // const PI = 3.14159 // SyntaxError + ``` + </Tab> + <Tab title="Reassignment"> + ```javascript + // var and let allow reassignment + var count = 1; + count = 2; // ✓ Fine + + let score = 100; + score = 200; // ✓ Fine + + // const prevents reassignment + const API_KEY = "abc123" + // API_KEY = "xyz789" // TypeError: Assignment to constant variable + + // BUT: const objects/arrays CAN be mutated! + const user = { name: "Alice" } + user.name = "Bob" // ✓ This works! + user.age = 25 // ✓ This works too! + // user = {} // ✗ This fails (reassignment) + ``` + </Tab> + <Tab title="Hoisting Behavior"> + ```javascript + function hoistingDemo() { + // var: hoisted and initialized as undefined + console.log(a); // undefined + var a = 1; + + // let: hoisted but NOT initialized (TDZ) + // console.log(b); // ReferenceError! + let b = 2; + + // const: same as let + // console.log(c); // ReferenceError! + const c = 3; + } + ``` + </Tab> +</Tabs> + +### The Classic for-loop Problem + +This is one of the most common JavaScript gotchas, and it perfectly illustrates why `let` is preferred over `var`: + +```javascript +// The Problem: var is function-scoped +for (var i = 0; i < 3; i++) { + setTimeout(() => { + console.log(i) + }, 100) +} +// Output: 3, 3, 3 (not 0, 1, 2!) + +// Why? There's only ONE 'i' variable shared across all iterations. +// By the time the setTimeout callbacks run, the loop has finished and i === 3. +``` + +The [`setTimeout()`](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout) callbacks all close over the same `i` variable, which equals `3` by the time they execute. (To understand why the callbacks don't run immediately, see our [Event Loop](/concepts/event-loop) guide.) + +```javascript +// The Solution: let is block-scoped +for (let i = 0; i < 3; i++) { + setTimeout(() => { + console.log(i) + }, 100) +} +// Output: 0, 1, 2 (correct!) + +// Why? Each iteration gets its OWN 'i' variable. +// Each setTimeout callback closes over a different 'i'. +``` + +<Tip> +**Modern Best Practice:** +1. Use `const` by default +2. Use `let` when you need to reassign +3. Avoid `var` entirely (legacy code only) + +This approach catches bugs at compile time and makes your intent clear. +</Tip> + +--- + +## Lexical Scope + +**Lexical scope** (also called **static scope**) means that the scope of a variable is determined by its position in the source code, not by how functions are called at runtime. + +```javascript +const outer = "I'm outside!"; + +function outerFunction() { + const middle = "I'm in the middle!"; + + function innerFunction() { + const inner = "I'm inside!"; + + // innerFunction can access all three variables + console.log(inner); // ✓ Own scope + console.log(middle); // ✓ Parent scope + console.log(outer); // ✓ Global scope + } + + innerFunction(); + // console.log(inner); // ✗ ReferenceError +} + +outerFunction(); +// console.log(middle); // ✗ ReferenceError +``` + +### The Scope Chain + +When JavaScript needs to find a variable, it walks up the **scope chain**. It starts from the current scope and moves outward until it finds the variable or reaches the global scope. + +<Steps> + <Step title="Look in Current Scope"> + JavaScript first checks if the variable exists in the current function/block scope. + </Step> + + <Step title="Look in Parent Scope"> + If not found, it checks the enclosing (parent) scope. + </Step> + + <Step title="Continue Up the Chain"> + This process continues up through all ancestor scopes. + </Step> + + <Step title="Reach Global Scope"> + Finally, it checks the global scope. If still not found, a `ReferenceError` is thrown. + </Step> +</Steps> + +``` +Variable Lookup: Where is 'x'? + +┌─────────────────────────────────────────────────┐ +│ Global Scope │ +│ x = "global" │ +│ │ +│ ┌─────────────────────────────────────────┐ │ +│ │ outer() Scope │ │ +│ │ x = "outer" │ │ +│ │ │ │ +│ │ ┌─────────────────────────────────┐ │ │ +│ │ │ inner() Scope │ │ │ +│ │ │ │ │ │ +│ │ │ console.log(x); │ │ │ +│ │ │ │ │ │ │ +│ │ │ ▼ │ │ │ +│ │ │ 1. Check inner() → not found │ │ │ +│ │ │ │ │ │ │ +│ │ └─────────│───────────────────────┘ │ │ +│ │ ▼ │ │ +│ │ 2. Check outer() → FOUND! "outer" │ │ +│ │ │ │ +│ └─────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────┘ + +Result: "outer" +``` + +### Variable Shadowing + +When an inner scope declares a variable with the same name as an outer scope, the inner variable "shadows" the outer one: + +```javascript +const name = "Global"; + +function greet() { + const name = "Function"; // Shadows global 'name' + + if (true) { + const name = "Block"; // Shadows function 'name' + console.log(name); // "Block" + } + + console.log(name); // "Function" +} + +greet(); +console.log(name); // "Global" +``` + +<Warning> +Shadowing can be confusing. While sometimes intentional, accidental shadowing is a common source of bugs. Many linters warn about this. +</Warning> + +--- + +## What is a Closure in JavaScript? + +A **[closure](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Closures)** is the combination of a function bundled together with references to its surrounding state (the lexical environment). In other words, a closure gives a function access to variables from an outer (enclosing) scope, even after that outer function has finished executing and returned. Every function in JavaScript creates a closure at creation time. + +Remember our office building analogy? A closure is like someone who worked in the private office, left the building, but still remembers exactly where everything was, and can still use that knowledge! + +### Every Function Creates a Closure + +In JavaScript, closures are created automatically every time you create a function. The function maintains a reference to its lexical environment. + +```javascript +function createGreeter(greeting) { + // 'greeting' is in createGreeter's scope + + return function(name) { + // This inner function is a closure! + // It "closes over" the 'greeting' variable + console.log(`${greeting}, ${name}!`); + }; +} + +const sayHello = createGreeter("Hello"); +const sayHola = createGreeter("Hola"); + +// createGreeter has finished executing, but... +sayHello("Alice"); // "Hello, Alice!" +sayHola("Bob"); // "Hola, Bob!" + +// The inner functions still remember their 'greeting' values! +``` + +### How Closures Work: Step by Step + +<Steps> + <Step title="Outer Function is Called"> + `createGreeter("Hello")` is called. A new execution context is created with `greeting = "Hello"`. + </Step> + + <Step title="Inner Function is Created"> + The inner function is created. It captures a reference to the current lexical environment (which includes `greeting`). + </Step> + + <Step title="Outer Function Returns"> + `createGreeter` returns the inner function and its execution context is (normally) cleaned up. + </Step> + + <Step title="But the Closure Survives!"> + Because the inner function holds a reference to the lexical environment, the `greeting` variable is NOT garbage collected. It survives! + </Step> + + <Step title="Closure is Invoked Later"> + When `sayHello("Alice")` is called, the function can still access `greeting` through its closure. + </Step> +</Steps> + +``` +After createGreeter("Hello") returns: + +┌──────────────────────────────────────┐ +│ sayHello (Function) │ +├──────────────────────────────────────┤ +│ [[Code]]: function(name) {...} │ +│ │ +│ [[Environment]]: ────────────────────────┐ +└──────────────────────────────────────┘ │ + ▼ + ┌────────────────────────────┐ + │ Lexical Environment │ + │ (Kept alive by closure!) │ + ├────────────────────────────┤ + │ greeting: "Hello" │ + └────────────────────────────┘ +``` + +--- + +## Closures in the Wild + +Closures aren't just a theoretical concept. You'll use them every day. Here are the patterns that make closures so powerful. + +### 1. Data Privacy & Encapsulation + +Closures let you create truly private variables in JavaScript: + +```javascript +function createCounter() { + let count = 0; // Private variable - no way to access directly! + + return { + increment() { + count++; + return count; + }, + decrement() { + count--; + return count; + }, + getCount() { + return count; + } + }; +} + +const counter = createCounter(); + +console.log(counter.getCount()); // 0 +console.log(counter.increment()); // 1 +console.log(counter.increment()); // 2 +console.log(counter.decrement()); // 1 + +// There's NO way to access 'count' directly! +console.log(counter.count); // undefined +``` + +<Tip> +This pattern is the foundation of the **Module Pattern**, widely used before ES6 modules became available. Learn more in our [IIFE, Modules and Namespaces](/concepts/iife-modules) guide. +</Tip> + +### 2. Function Factories + +Closures let you create specialized functions on the fly: + +```javascript +function createMultiplier(multiplier) { + return function(number) { + return number * multiplier; + }; +} + +const double = createMultiplier(2); +const triple = createMultiplier(3); +const tenX = createMultiplier(10); + +console.log(double(5)); // 10 +console.log(triple(5)); // 15 +console.log(tenX(5)); // 50 + +// Each function "remembers" its own multiplier +``` + +This pattern works great with the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) for creating reusable API clients: + +```javascript +// Real-world example: API request factories +function createApiClient(baseUrl) { + return { + get(endpoint) { + return fetch(`${baseUrl}${endpoint}`); + }, + post(endpoint, data) { + return fetch(`${baseUrl}${endpoint}`, { + method: 'POST', + body: JSON.stringify(data) + }); + } + }; +} + +const githubApi = createApiClient('https://api.github.com'); +const myApi = createApiClient('https://myapp.com/api'); + +// Each client remembers its baseUrl +githubApi.get('/users/leonardomso'); +myApi.get('/users/1'); +``` + +### 3. Preserving State in Callbacks & Event Handlers + +Closures are essential for maintaining state in asynchronous code. When you use [`addEventListener()`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) to attach event handlers, those handlers can close over variables from their outer scope: + +```javascript +function setupClickCounter(buttonId) { + let clicks = 0; // This variable persists across clicks! + + const button = document.getElementById(buttonId); + + button.addEventListener('click', function() { + clicks++; + console.log(`Button clicked ${clicks} time${clicks === 1 ? '' : 's'}`); + }); +} + +setupClickCounter('myButton'); +// Each click increments the same 'clicks' variable +// Click 1: "Button clicked 1 time" +// Click 2: "Button clicked 2 times" +// Click 3: "Button clicked 3 times" +``` + +### 4. Memoization (Caching Results) + +Closures enable efficient caching of expensive computations: + +```javascript +function createMemoizedFunction(fn) { + const cache = {}; // Cache persists across calls! + + return function(arg) { + if (arg in cache) { + console.log('Returning cached result'); + return cache[arg]; + } + + console.log('Computing result'); + const result = fn(arg); + cache[arg] = result; + return result; + }; +} + +// Expensive operation: calculate factorial +function factorial(n) { + if (n <= 1) return 1; + return n * factorial(n - 1); +} + +const memoizedFactorial = createMemoizedFunction(factorial); + +console.log(memoizedFactorial(5)); // Computing result → 120 +console.log(memoizedFactorial(5)); // Returning cached result → 120 +console.log(memoizedFactorial(5)); // Returning cached result → 120 +``` + +--- + +## Common Mistakes and Pitfalls + +Understanding scope and closures means understanding where things go wrong. These are the mistakes that trip up even experienced developers. + +### The #1 Closure Interview Question + +This is the classic closure trap. Almost everyone gets it wrong the first time: + +### The Problem + +```javascript +// What does this print? +for (var i = 0; i < 3; i++) { + setTimeout(function() { + console.log(i); + }, 1000); +} + +// Most people expect: 0, 1, 2 +// Actual output: 3, 3, 3 +``` + +### Why Does This Happen? + +``` +What actually happens: + + TIME ════════════════════════════════════════════════════► + + ┌─────────────────────────────────────────────────────────┐ + │ IMMEDIATELY (milliseconds): │ + │ │ + │ Loop iteration 1: i = 0, schedule callback │ + │ Loop iteration 2: i = 1, schedule callback │ + │ Loop iteration 3: i = 2, schedule callback │ + │ Loop ends: i = 3 │ + │ │ + │ All 3 callbacks point to the SAME 'i' variable ──┐ │ + └─────────────────────────────────────────────────────│───┘ + │ + ▼ + ┌─────────────────────────────────────────────────────────┐ + │ ~1 SECOND LATER: │ + │ │ + │ callback1 runs: "What's i?" → i is 3 → prints 3 │ + │ callback2 runs: "What's i?" → i is 3 → prints 3 │ + │ callback3 runs: "What's i?" → i is 3 → prints 3 │ + │ │ + └─────────────────────────────────────────────────────────┘ + + Result: 3, 3, 3 (not 0, 1, 2!) +``` + +### The Solutions + +<Tabs> + <Tab title="Solution 1: Use let"> + The simplest modern solution. `let` creates a new binding for each iteration: + + ```javascript + for (let i = 0; i < 3; i++) { + setTimeout(function() { + console.log(i); + }, 1000); + } + // Output: 0, 1, 2 ✓ + ``` + </Tab> + <Tab title="Solution 2: IIFE"> + Pre-ES6 solution using an Immediately Invoked Function Expression: + + ```javascript + for (var i = 0; i < 3; i++) { + (function(j) { + setTimeout(function() { + console.log(j); + }, 1000); + })(i); // Pass i as argument, creating a new 'j' each time + } + // Output: 0, 1, 2 ✓ + ``` + </Tab> + <Tab title="Solution 3: forEach"> + Using array methods, which naturally create new scope per iteration: + + ```javascript + [0, 1, 2].forEach(function(i) { + setTimeout(function() { + console.log(i); + }, 1000); + }); + // Output: 0, 1, 2 ✓ + ``` + </Tab> +</Tabs> + +### Memory Leaks from Closures + +Closures are powerful, but they come with responsibility. Since closures keep references to their outer scope variables, those variables can't be [garbage collected](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_management#garbage_collection). + +### Potential Memory Leaks + +```javascript +function createHeavyClosure() { + const hugeData = new Array(1000000).fill('x'); // Large data + + return function() { + // This reference to hugeData keeps the entire array in memory + console.log(hugeData.length); + }; +} + +const leakyFunction = createHeavyClosure(); +// hugeData is still in memory because the closure references it +``` + +<Note> +Modern JavaScript engines like V8 can optimize closures that don't actually use outer variables. However, it's best practice to assume referenced variables are retained and explicitly clean up large data when you're done with it. +</Note> + +### Breaking Closure References + +When you're done with a closure, explicitly break the reference. Use [`removeEventListener()`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener) to clean up event handlers: + +```javascript +function setupHandler(element) { + // Imagine this returns a large dataset + const largeData = { users: new Array(10000).fill({ name: 'User' }) }; + + const handler = function() { + console.log(`Processing ${largeData.users.length} users`); + }; + + element.addEventListener('click', handler); + + // Return a cleanup function + return function cleanup() { + element.removeEventListener('click', handler); + // Now handler and largeData can be garbage collected + }; +} + +const button = document.getElementById('myButton'); +const cleanup = setupHandler(button); + +// Later, when you're done with this functionality: +cleanup(); // Removes listener, allows memory to be freed +``` + +<Tip> +**Best Practices:** +1. Don't capture more than you need in closures +2. Set closure references to `null` when done +3. Remove event listeners when components unmount +4. Be especially careful in loops and long-lived applications +</Tip> + +--- + +## Key Takeaways + +<Info> +**The key things to remember about Scope & Closures:** + +1. **Scope = Variable Visibility** — It determines where variables can be accessed + +2. **Three types of scope**: Global (everywhere), Function (`var`), Block (`let`/`const`) + +3. **Lexical scope is static** — Determined by code position, not runtime behavior + +4. **Scope chain** — JavaScript looks up variables from inner to outer scope + +5. **`let` and `const` are block-scoped** — Prefer them over `var` + +6. **Temporal Dead Zone** — `let`/`const` can't be accessed before declaration + +7. **Closure = Function + Its Lexical Environment** — Functions "remember" where they were created + +8. **Closures enable**: Data privacy, function factories, stateful callbacks, memoization + +9. **Watch for the loop gotcha** — Use `let` instead of `var` in loops with async callbacks + +10. **Mind memory** — Closures keep references alive; clean up when done +</Info> + +--- + +## Test Your Knowledge + +<AccordionGroup> + <Accordion title="Question 1: What are the three types of scope in JavaScript?"> + **Answer:** + + 1. **Global Scope** — Variables declared outside any function or block; accessible everywhere + 2. **Function Scope** — Variables declared with `var` inside a function; accessible only within that function + 3. **Block Scope** — Variables declared with `let` or `const` inside a block `{}`; accessible only within that block + + ```javascript + const global = "everywhere"; // Global scope + + function example() { + var functionScoped = "function"; // Function scope + + if (true) { + let blockScoped = "block"; // Block scope + } + } + ``` + </Accordion> + + <Accordion title="Question 2: What is the Temporal Dead Zone?"> + **Answer:** The Temporal Dead Zone (TDZ) is the period between entering a scope and the actual declaration of a `let` or `const` variable. During this time, the variable exists but cannot be accessed. Doing so throws a `ReferenceError`. + + ```javascript + function example() { + // TDZ starts for 'x' + console.log(x); // ReferenceError! + // TDZ continues... + let x = 10; // TDZ ends + console.log(x); // 10 ✓ + } + ``` + + The TDZ helps catch bugs where you accidentally use variables before they're initialized. + </Accordion> + + <Accordion title="Question 3: What is lexical scope?"> + **Answer:** Lexical scope (also called static scope) means that the accessibility of variables is determined by their physical position in the source code at write time, not by how or where functions are called at runtime. + + Inner functions have access to variables declared in their outer functions because of where they are written, not because of when they're invoked. + + ```javascript + function outer() { + const message = "Hello"; + + function inner() { + console.log(message); // Can access 'message' because of lexical scope + } + + return inner; + } + + const fn = outer(); + fn(); // "Hello" — still works even though outer() has returned + ``` + </Accordion> + + <Accordion title="Question 4: What is a closure?"> + **Answer:** A closure is a function combined with references to its surrounding lexical environment. In simpler terms, a closure is a function that "remembers" the variables from the scope where it was created, even when executed outside that scope. + + ```javascript + function createCounter() { + let count = 0; // This variable is "enclosed" in the closure + + return function() { + count++; + return count; + }; + } + + const counter = createCounter(); + console.log(counter()); // 1 + console.log(counter()); // 2 + // 'count' persists because of the closure + ``` + + Every function in JavaScript creates a closure. The term usually refers to situations where this behavior is notably useful or surprising. + </Accordion> + + <Accordion title="Question 5: What will this code output and why?"> + ```javascript + for (var i = 0; i < 3; i++) { + setTimeout(() => console.log(i), 100); + } + ``` + + **Answer:** It outputs `3, 3, 3`. + + **Why?** Because `var` is function-scoped (not block-scoped), there's only ONE `i` variable shared across all iterations. By the time the `setTimeout` callbacks execute (after ~100ms), the loop has already completed and `i` equals `3`. + + **Fix:** Use `let` instead of `var`: + ```javascript + for (let i = 0; i < 3; i++) { + setTimeout(() => console.log(i), 100); + } + // Outputs: 0, 1, 2 + ``` + + With `let`, each iteration gets its own `i` variable, and each callback closes over a different value. + </Accordion> + + <Accordion title="Question 6: When would you use a closure in real code?"> + **Answer:** Common practical uses for closures include: + + 1. **Data Privacy** — Creating private variables that can't be accessed directly: + ```javascript + function createWallet(initial) { + let balance = initial; + return { + spend(amount) { balance -= amount; }, + getBalance() { return balance; } + }; + } + ``` + + 2. **Function Factories** — Creating specialized functions: + ```javascript + function createTaxCalculator(rate) { + return (amount) => amount * rate; + } + const calculateVAT = createTaxCalculator(0.20); + ``` + + 3. **Maintaining State in Callbacks** — Event handlers, timers, API calls: + ```javascript + function setupLogger(prefix) { + return (message) => console.log(`[${prefix}] ${message}`); + } + ``` + + 4. **Memoization/Caching** — Storing computed results: + ```javascript + function memoize(fn) { + const cache = {}; + return (arg) => cache[arg] ?? (cache[arg] = fn(arg)); + } + ``` + </Accordion> +</AccordionGroup> + +--- + +## Related Concepts + +<CardGroup cols={2}> + <Card title="Call Stack" icon="layer-group" href="/concepts/call-stack"> + How JavaScript tracks function execution and manages scope + </Card> + <Card title="IIFE, Modules and Namespaces" icon="box" href="/concepts/iife-modules"> + Patterns that leverage scope for encapsulation + </Card> + <Card title="this, call, apply and bind" icon="bullseye" href="/concepts/this-call-apply-bind"> + Understanding execution context alongside scope + </Card> + <Card title="Higher Order Functions" icon="arrows-repeat" href="/concepts/higher-order-functions"> + Functions that return functions often create closures + </Card> + <Card title="Currying & Composition" icon="wand-magic-sparkles" href="/concepts/currying-composition"> + Advanced patterns built on closures + </Card> +</CardGroup> + +--- + +## Reference + +<CardGroup cols={2}> + <Card title="Closures — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Closures"> + Official MDN documentation on closures and lexical scoping + </Card> + <Card title="Scope — MDN Glossary" icon="book" href="https://developer.mozilla.org/en-US/docs/Glossary/Scope"> + MDN glossary entry explaining scope in JavaScript + </Card> + <Card title="var — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/var"> + Reference for the var keyword, function scope, and hoisting + </Card> + <Card title="let — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let"> + Reference for the let keyword and block scope + </Card> + <Card title="const — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const"> + Reference for the const keyword and immutable bindings + </Card> + <Card title="Closures — JavaScript.Info" icon="book" href="https://javascript.info/closure"> + In-depth tutorial on closures and lexical environment + </Card> +</CardGroup> + +## Books + +<Card title="You Don't Know JS Yet: Scope & Closures — Kyle Simpson" icon="book" href="https://github.com/getify/You-Dont-Know-JS/tree/2nd-ed/scope-closures"> + The definitive deep-dive into JavaScript scope and closures. Free to read online. This book will transform your understanding of how JavaScript really works. +</Card> + +## Articles + +<CardGroup cols={2}> + <Card title="Var, Let, and Const – What's the Difference?" icon="newspaper" href="https://www.freecodecamp.org/news/var-let-and-const-whats-the-difference/"> + Clear FreeCodeCamp guide comparing the three variable declaration keywords with practical examples. + </Card> + <Card title="JavaScript Scope and Closures" icon="newspaper" href="https://css-tricks.com/javascript-scope-closures/"> + Zell Liew's comprehensive CSS-Tricks article covering both scope and closures in one excellent resource. + </Card> + <Card title="whatthefuck.is · A Closure" icon="newspaper" href="https://whatthefuck.is/closure"> + Dan Abramov's clear, concise explanation of closures. Perfect for the "aha moment." + </Card> + <Card title="I never understood JavaScript closures" icon="newspaper" href="https://medium.com/dailyjs/i-never-understood-javascript-closures-9663703368e8"> + Olivier De Meulder's article that has helped countless developers finally understand closures. + </Card> + <Card title="The Difference Between Function and Block Scope" icon="newspaper" href="https://medium.com/@josephcardillo/the-difference-between-function-and-block-scope-in-javascript-4296b2322abe"> + Joseph Cardillo's focused explanation of how var differs from let and const in terms of scope. + </Card> + <Card title="Closures: Using Memoization" icon="newspaper" href="https://dev.to/steelvoltage/closures-using-memoization-3597"> + Brian Barbour's practical guide showing how closures enable powerful caching patterns. + </Card> +</CardGroup> + +## Tools + +<Card title="JavaScript Tutor — Visualize Code Execution" icon="play" href="https://pythontutor.com/javascript.html"> + Step through JavaScript code and see how closures capture variables in real-time. Visualize the scope chain, execution contexts, and how functions "remember" their environment. Perfect for understanding closures visually. +</Card> + +## Courses + +<Card title="JavaScript: Understanding the Weird Parts (First 3.5 Hours)" icon="graduation-cap" href="https://www.youtube.com/watch?v=Bv_5Zv5c-Ts"> + Free preview of Anthony Alicea's acclaimed course. Excellent coverage of scope, closures, and execution contexts. +</Card> + +## Videos + +<CardGroup cols={2}> + <Card title="JavaScript The Hard Parts: Closure, Scope & Execution Context" icon="video" href="https://www.youtube.com/watch?v=XTAzsODSCsM"> + Will Sentance draws out execution contexts and the scope chain on a whiteboard as code runs. This visual approach makes the "how" of closures click. + </Card> + <Card title="Closures in JavaScript" icon="video" href="https://youtu.be/qikxEIxsXco"> + Akshay Saini's popular Namaste JavaScript episode with clear visual explanations. + </Card> + <Card title="Closures — Fun Fun Function" icon="video" href="https://www.youtube.com/watch?v=CQqwU2Ixu-U"> + Mattias Petter Johansson's entertaining and educational take on closures. + </Card> + <Card title="Learn Closures In 7 Minutes" icon="video" href="https://www.youtube.com/watch?v=3a0I8ICR1Vg"> + Web Dev Simplified's concise, beginner-friendly closure explanation. + </Card> +</CardGroup> diff --git a/docs/concepts/this-call-apply-bind.mdx b/docs/concepts/this-call-apply-bind.mdx new file mode 100644 index 00000000..3827cdd5 --- /dev/null +++ b/docs/concepts/this-call-apply-bind.mdx @@ -0,0 +1,1481 @@ +--- +title: "this, call, apply, and bind: How Context Works in JavaScript" +sidebarTitle: "this, call, apply, and bind: How Context Works" +description: "Learn how JavaScript's 'this' keyword works and how to control context binding. Understand the 5 binding rules, call/apply/bind methods, arrow functions, and common pitfalls." +--- + +Why does `this` sometimes point to the wrong object? Why does your method work perfectly when called directly, but break when passed as a callback? And how do **[`call`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call)**, **[`apply`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply)**, and **[`bind`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind)** let you take control? + +```javascript +const user = { + name: "Alice", + greet() { + return `Hi, I'm ${this.name}`; + } +}; + +user.greet(); // "Hi, I'm Alice" - works! +const greet = user.greet; +greet(); // "Hi, I'm undefined" - broken! +``` + +The **[`this`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this)** keyword is one of JavaScript's most confusing features, but it follows specific rules. Once you understand them, you'll never be confused again. + +<Info> +**What you'll learn in this guide:** +- What `this` actually is and why it's determined at call time +- The 5 binding rules that determine `this` (in priority order) +- How `call()`, `apply()`, and `bind()` work and when to use each +- Arrow functions and why they handle `this` differently +- Common pitfalls and how to avoid them +</Info> + +<Warning> +**Prerequisite:** This guide builds on [Scope & Closures](/concepts/scope-and-closures). Understanding scope will help you see why `this` behaves differently than regular variables. +</Warning> + +--- + +## The Pronoun "I": A Real-World Analogy + +Think about the word "I" in everyday conversation. It's a simple word, but its meaning changes completely depending on **who is speaking**: + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ THE PRONOUN "I" │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ Alice says: "I am a developer" │ +│ ↓ │ +│ "I" = Alice │ +│ │ +│ Bob says: "I am a designer" │ +│ ↓ │ +│ "I" = Bob │ +│ │ +│ The SAME word "I" refers to DIFFERENT people │ +│ depending on WHO is speaking! │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +This is exactly how `this` works in JavaScript! The keyword `this` is like the pronoun "I". It refers to different objects depending on **who is "speaking"** (which object is calling the function). + +```javascript +const alice = { + name: "Alice", + introduce() { + return "I am " + this.name; // "I" = this = alice + } +}; + +const bob = { + name: "Bob", + introduce() { + return "I am " + this.name; // "I" = this = bob + } +}; + +alice.introduce(); // "I am Alice" +bob.introduce(); // "I am Bob" +``` + +But here's where JavaScript gets interesting. What if Alice could make Bob say her words? Like a ventriloquist making a puppet speak? + +```javascript +// Alice borrows Bob's voice to introduce herself +bob.introduce.call(alice); // "I am Alice" (Bob's function, Alice's this) +``` + +That's what `call`, `apply`, and `bind` do. They let you control **who "I" refers to**, regardless of which function is speaking. + +--- + +## What is `this` in JavaScript? + +The **`this`** keyword is a special identifier that JavaScript automatically creates in every function execution context. It refers to the object that is currently executing the code, typically the object that "owns" the method being called. Unlike most languages where `this` is fixed at definition time, JavaScript determines `this` dynamically at **call time**, based on how a function is invoked. + +### The Key Insight: Call-Time Binding + +Here's what makes JavaScript different from many other languages: + +> **`this` is determined when the function is CALLED, not when it's defined.** + +This is called **dynamic binding**, and it's both powerful and confusing. The same function can have different `this` values depending on how you call it: + +```javascript +function showThis() { + return this; +} + +const obj = { showThis }; + +// Same function, different this values: +showThis(); // undefined (strict mode) or globalThis +obj.showThis(); // obj (the object before the dot) +showThis.call({}); // {} (explicitly specified) +``` + +In non-strict mode, plain function calls return **[`globalThis`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis)** (the global object: `window` in browsers, `global` in Node.js). + +### Why Does JavaScript Work This Way? + +This design allows for incredible flexibility: + +1. **Method sharing**: Multiple objects can share the same function +2. **Dynamic behavior**: Functions can work with any object that has the right properties +3. **Borrowing methods**: You can use methods from one object on another + +```javascript +// One function, many objects +function greet() { + return `Hello, I'm ${this.name}!`; +} + +const alice = { name: "Alice", greet }; +const bob = { name: "Bob", greet }; +const charlie = { name: "Charlie", greet }; + +alice.greet(); // "Hello, I'm Alice!" +bob.greet(); // "Hello, I'm Bob!" +charlie.greet(); // "Hello, I'm Charlie!" +``` + +The trade-off? You need to understand the rules that determine `this`. Let's dive in. + +--- + +## The 5 Binding Rules (Priority Order) + +When JavaScript needs to figure out what `this` refers to, it follows these rules **in order of priority**. Higher priority rules override lower ones. + +``` +BINDING RULES (Highest to Lowest Priority) +┌─────────────────────────────────────────┐ +│ 1. new Binding (Highest) │ +├─────────────────────────────────────────┤ +│ 2. Explicit Binding (call/apply/ │ +│ bind) │ +├─────────────────────────────────────────┤ +│ 3. Implicit Binding (method call) │ +├─────────────────────────────────────────┤ +│ 4. Default Binding (plain call) │ +├─────────────────────────────────────────┤ +│ 5. Arrow Functions (lexical) │ +│ (Special case - no own this) │ +└─────────────────────────────────────────┘ +``` + +<Note> +Arrow functions are listed last not because they're lowest priority, but because they work differently. They don't have their own `this` at all. We'll cover them in detail. +</Note> + +--- + +### Rule 1: `new` Binding (Highest Priority) + +When a function is called with the **[`new`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new)** keyword, `this` is set to a **brand new object** that's automatically created. + +```javascript +class Person { + constructor(name) { + // 'this' is the new object being created + this.name = name; + this.greet = function() { + return `Hi, I'm ${this.name}`; + }; + } +} + +const alice = new Person("Alice"); +console.log(alice.name); // "Alice" +console.log(alice.greet()); // "Hi, I'm Alice" +``` + +#### What `new` Does Under the Hood + +When you call `new Person("Alice")`, JavaScript performs these 4 steps: + +<Steps> + <Step title="Create an empty object"> + A brand new empty object is created: `{}` + </Step> + + <Step title="Link the prototype"> + The new object's internal `[[Prototype]]` is set to the constructor's **[`prototype`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/prototype)** property. + + ```javascript + // Conceptually: + newObject.__proto__ = Person.prototype; + ``` + </Step> + + <Step title="Bind this and execute"> + The constructor function is called with `this` bound to the new object. This is where your constructor code runs. + + ```javascript + // Conceptually: + Person.call(newObject, "Alice"); + ``` + </Step> + + <Step title="Return the object"> + If the constructor doesn't explicitly return an object, the new object is returned automatically. + + ```javascript + // Conceptually: + return newObject; + ``` + </Step> +</Steps> + +Here's a simplified implementation of what `new` does: + +```javascript +// What 'new' does behind the scenes +function simulateNew(Constructor, ...args) { + // Step 1: Create empty object + const newObject = {}; + + // Step 2: Link prototype if it's an object + // (If prototype isn't an object, newObject keeps Object.prototype) + if (Constructor.prototype !== null && typeof Constructor.prototype === 'object') { + Object.setPrototypeOf(newObject, Constructor.prototype); + } + + // Step 3: Bind this and execute + const result = Constructor.apply(newObject, args); + + // Step 4: Return object (unless constructor returns a non-primitive) + return result !== null && typeof result === 'object' ? result : newObject; +} + +// These are equivalent: +const alice1 = new Person("Alice"); +const alice2 = simulateNew(Person, "Alice"); +``` + +#### ES6 Classes: The Modern Syntax + +With ES6 classes, the syntax is cleaner but the behavior is identical: + +```javascript +class Rectangle { + constructor(width, height) { + this.width = width; // 'this' = new Rectangle instance + this.height = height; + } + + getArea() { + return this.width * this.height; // 'this' = the instance + } +} + +const rect = new Rectangle(10, 5); +console.log(rect.getArea()); // 50 +``` + +<Tip> +For more on constructors, the `new` keyword, and prototypes, see the [Object Creation & Prototypes](/concepts/object-creation-prototypes) concept page. +</Tip> + +--- + +### Rule 2: Explicit Binding (`call`, `apply`, `bind`) + +You can explicitly specify what `this` should be using `call()`, `apply()`, or `bind()`. This overrides implicit and default binding. + +```javascript +function introduce() { + return `I'm ${this.name}, a ${this.role}`; +} + +const alice = { name: "Alice", role: "developer" }; +const bob = { name: "Bob", role: "designer" }; + +// Explicitly set 'this' to alice +introduce.call(alice); // "I'm Alice, a developer" + +// Explicitly set 'this' to bob +introduce.call(bob); // "I'm Bob, a designer" +``` + +We'll cover `call`, `apply`, and `bind` in detail in the next section. For now, just know that explicit binding has higher priority than implicit binding: + +```javascript +const alice = { + name: "Alice", + greet() { + return `Hi, I'm ${this.name}`; + } +}; + +const bob = { name: "Bob" }; + +// Even though we're calling alice.greet(), we can override 'this' +alice.greet.call(bob); // "Hi, I'm Bob" (explicit wins!) +``` + +--- + +### Rule 3: Implicit Binding (Method Call) + +When a function is called as a **method of an object** (using dot notation), `this` is set to the object **before the dot**. + +```javascript +const user = { + name: "Alice", + greet() { + return `Hello, I'm ${this.name}`; + } +}; + +// The object before the dot becomes 'this' +user.greet(); // "Hello, I'm Alice" (this = user) +``` + +#### The "Left of the Dot" Rule + +A simple way to remember: **look left of the dot** when the function is called. + +```javascript +const company = { + name: "TechCorp", + department: { + name: "Engineering", + getName() { + return this.name; + } + } +}; + +// What's left of the dot at call time? +company.department.getName(); // "Engineering" (this = department) +``` + +<Warning> +**Common trap**: It's the object immediately before the dot that matters, not the outermost object. In the example above, `this` is `department`, not `company`. +</Warning> + +#### The Implicit Binding Gotcha: Lost Context + +This trips up many developers. When you **extract a method** from an object, it loses its implicit binding: + +```javascript +const user = { + name: "Alice", + greet() { + return `Hello, I'm ${this.name}`; + } +}; + +// This works +user.greet(); // "Hello, I'm Alice" + +// But extracting the method loses 'this'! +const greetFn = user.greet; +greetFn(); // "Hello, I'm undefined" (strict mode: this = undefined) +``` + +Why? Because `greetFn()` is a plain function call. There's no dot, so implicit binding doesn't apply. We fall through to default binding. + +``` +IMPLICIT BINDING LOST +┌─────────────────────────────────────────────────────────────┐ +│ │ +│ user.greet() │ +│ ↑ │ +│ └── Object before dot → this = user ✓ │ +│ │ +│ const greetFn = user.greet; │ +│ greetFn() │ +│ ↑ │ +│ └── No dot! → Default binding → this = undefined ✗ │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +This happens constantly with: +- Callbacks: `setTimeout(user.greet, 1000)` +- Event handlers: `button.addEventListener('click', user.greet)` +- Array methods: `[1,2,3].forEach(user.process)` + +We'll cover solutions in the "Gotchas" section. + +--- + +### Rule 4: Default Binding (Plain Function Call) + +When a function is called without any of the above conditions, **default binding** applies. + +In **[strict mode](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode)** (which you should always use): `this` is `undefined`. + +In non-strict mode: `this` is the global object (`window` in browsers, `global` in Node.js). + +```javascript +"use strict"; + +function showThis() { + return this; +} + +showThis(); // undefined (strict mode) +``` + +```javascript +// Without strict mode (not recommended) +function showThis() { + return this; +} + +showThis(); // window (in browser) or global (in Node.js) +``` + +<Warning> +**Always use strict mode!** Non-strict mode's default binding to `globalThis` is dangerous. It can accidentally create or modify global variables, leading to hard-to-find bugs. + +ES6 modules and classes are automatically in strict mode. +</Warning> + +#### When Default Binding Applies + +Default binding kicks in when: + +1. **Plain function call**: `myFunction()` +2. **IIFE**: `(function() { ... })()` +3. **Callback without binding**: `setTimeout(function() { ... }, 100)` + +```javascript +"use strict"; + +// All of these use default binding (this = undefined) +function regularFunction() { + return this; +} + +regularFunction(); // undefined + +(function() { + return this; // undefined +})(); + +setTimeout(function() { + console.log(this); // undefined +}, 100); +``` + +--- + +### Rule 5: Arrow Functions (Lexical `this`) + +**[Arrow functions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions)** are **special**. They don't have their own `this` binding at all. Instead, they **inherit `this`** from their enclosing scope at the time they're defined. + +```javascript +const user = { + name: "Alice", + + // Regular function: 'this' is determined by how it's called + regularGreet: function() { + return `Hi, I'm ${this.name}`; + }, + + // Arrow function: 'this' is inherited from where it's defined + arrowGreet: () => { + return `Hi, I'm ${this.name}`; + } +}; + +user.regularGreet(); // "Hi, I'm Alice" (this = user) +user.arrowGreet(); // "Hi, I'm undefined" (this = enclosing scope, not user!) +``` + +Wait, why is `arrowGreet` showing `undefined`? Because the arrow function was defined in the object literal, and the enclosing scope at that point is the module/global scope, not the `user` object. + +#### Where Arrow Functions Shine + +Arrow functions are perfect for **callbacks** where you want to preserve the outer `this`: + +```javascript +class Counter { + constructor() { + this.count = 0; + } + + // Problem: regular function loses 'this' in callback + startBroken() { + setInterval(function() { + this.count++; // ERROR: 'this' is undefined! + console.log(this.count); + }, 1000); + } + + // Solution: arrow function preserves 'this' + startFixed() { + setInterval(() => { + this.count++; // Works! 'this' is the Counter instance + console.log(this.count); + }, 1000); + } +} +``` + +#### Arrow Functions Cannot Be Rebound + +You cannot change an arrow function's `this` using `call`, `apply`, or `bind`: + +```javascript +const arrowFn = () => this; + +const obj = { name: "Object" }; + +// These all return the same thing - the lexical 'this' +arrowFn(); // lexical this +arrowFn.call(obj); // lexical this (call is ignored!) +arrowFn.apply(obj); // lexical this (apply is ignored!) +arrowFn.bind(obj)(); // lexical this (bind is ignored!) +``` + +#### Arrow Functions as Class Fields + +A common modern pattern is using arrow functions as class methods: + +```javascript +class Button { + constructor(label) { + this.label = label; + } + + // Arrow function as class field - 'this' is always the instance + handleClick = () => { + console.log(`Button "${this.label}" clicked`); + } +} + +const btn = new Button("Submit"); + +// Works even when extracted! +const handler = btn.handleClick; +handler(); // "Button "Submit" clicked" ✓ + +// Works in event listeners! +document.querySelector('button').addEventListener('click', btn.handleClick); +``` + +<Tip> +This pattern is widely used in React class components and other UI frameworks to ensure event handlers always have the correct `this`. +</Tip> + +--- + +## The Decision Flowchart + +When you need to figure out what `this` is, follow this flowchart: + +``` + ┌─────────────────────────┐ + │ Is it an arrow │ + │ function? │ + └───────────┬─────────────┘ + │ │ + YES ◄──┘ └──► NO + │ │ + ▼ ▼ + ┌─────────────────┐ ┌─────────────────────┐ + │ this = enclosing│ │ Was it called with │ + │ scope's this │ │ 'new'? │ + │ (DONE) │ └──────────┬──────────┘ + └─────────────────┘ │ │ + YES ◄─┘ └──► NO + │ │ + ▼ ▼ + ┌─────────────────┐ ┌─────────────────────┐ + │ this = new │ │ Was call/apply/bind │ + │ object │ │ used? │ + │ (DONE) │ └──────────┬──────────┘ + └─────────────────┘ │ │ + YES ◄──┘ └──► NO + │ │ + ▼ ▼ + ┌─────────────────┐ ┌─────────────────────┐ + │ this = specified│ │ Was it called as │ + │ object │ │ obj.method()? │ + │ (DONE) │ └──────────┬──────────┘ + └─────────────────┘ │ │ + YES ◄──┘ └──► NO + │ │ + ▼ ▼ + ┌─────────────────┐ ┌─────────────────┐ + │ this = obj │ │ Default binding:│ + │ (left of dot) │ │ this = undefined│ + │ (DONE) │ │ (strict mode) │ + └─────────────────┘ └─────────────────┘ +``` + +--- + +## How Do `call()`, `apply()`, and `bind()` Work? + +These three methods give you explicit control over `this`. They're built into every function in JavaScript. + +### Quick Comparison + +| Method | Invokes Function? | Arguments | Returns | +|--------|-------------------|-----------|---------| +| `call()` | Yes, immediately | Individual: `call(this, a, b, c)` | Function result | +| `apply()` | Yes, immediately | Array: `apply(this, [a, b, c])` | Function result | +| `bind()` | No | Individual: `bind(this, a, b)` | New function | + +**Memory trick:** +- **C**all = **C**ommas (arguments separated by commas) +- **A**pply = **A**rray (arguments in an array) +- **B**ind = **B**ack later (returns a function for later use) + +--- + +### `call()` — Call with This + +The `call()` method calls a function with a specified `this` value and arguments provided **individually**. + +**Syntax:** +```javascript +func.call(thisArg, arg1, arg2, ...) +``` + +**Basic example:** +```javascript +function greet(greeting, punctuation) { + return `${greeting}, I'm ${this.name}${punctuation}`; +} + +const alice = { name: "Alice" }; +const bob = { name: "Bob" }; + +greet.call(alice, "Hello", "!"); // "Hello, I'm Alice!" +greet.call(bob, "Hi", "..."); // "Hi, I'm Bob..." +``` + +#### Use Case: Method Borrowing + +`call()` is perfect for borrowing methods from one object to use on another: + +```javascript +const arrayLike = { + 0: "a", + 1: "b", + 2: "c", + length: 3 +}; + +// arrayLike doesn't have array methods, but we can borrow them! +const result = Array.prototype.slice.call(arrayLike); +console.log(result); // ["a", "b", "c"] + +const joined = Array.prototype.join.call(arrayLike, "-"); +console.log(joined); // "a-b-c" +``` + +#### Use Case: Calling Parent Methods + +```javascript +class Animal { + constructor(name) { + this.name = name; + } + + speak() { + return `${this.name} makes a sound`; + } +} + +class Dog extends Animal { + speak() { + // Call parent method with 'this' context + const parentSays = Animal.prototype.speak.call(this); + return `${parentSays}. ${this.name} barks!`; + } +} + +const dog = new Dog("Rex"); +dog.speak(); // "Rex makes a sound. Rex barks!" +``` + +--- + +### `apply()` — Apply with Array + +The `apply()` method is almost identical to `call()`, but arguments are passed as an **array** (or array-like object). + +**Syntax:** +```javascript +func.apply(thisArg, [arg1, arg2, ...]) +``` + +**Basic example:** +```javascript +function greet(greeting, punctuation) { + return `${greeting}, I'm ${this.name}${punctuation}`; +} + +const alice = { name: "Alice" }; + +// Same result as call(), but args in an array +greet.apply(alice, ["Hello", "!"]); // "Hello, I'm Alice!" +``` + +#### Classic Use Case: Finding Max/Min + +Before ES6, `apply()` was the way to use `Math.max()` with an array: + +```javascript +const numbers = [5, 2, 9, 1, 7]; + +// Old way with apply +const max = Math.max.apply(null, numbers); // 9 +const min = Math.min.apply(null, numbers); // 1 +``` + +<Tip> +**Modern alternative:** Use the spread operator instead! + +```javascript +const numbers = [5, 2, 9, 1, 7]; +const max = Math.max(...numbers); // 9 +const min = Math.min(...numbers); // 1 +``` + +The spread syntax is cleaner and more readable. Use `apply()` mainly when you need to set `this` AND spread arguments. +</Tip> + +#### When Arguments Are Already an Array + +`apply()` shines when your arguments are already in array form: + +```javascript +function introduce(greeting, role, company) { + return `${greeting}! I'm ${this.name}, ${role} at ${company}.`; +} + +const alice = { name: "Alice" }; +const args = ["Hello", "engineer", "TechCorp"]; + +// When args are already an array, apply is natural +introduce.apply(alice, args); // "Hello! I'm Alice, engineer at TechCorp." + +// With call, you'd need to spread +introduce.call(alice, ...args); // Same result +``` + +--- + +### `bind()` — Bind for Later + +The `bind()` method is different from `call()` and `apply()`. It doesn't call the function immediately. Instead, it returns a **new function** with `this` permanently bound. + +**Syntax:** +```javascript +const boundFunc = func.bind(thisArg, arg1, arg2, ...) +``` + +**Basic example:** +```javascript +function greet() { + return `Hello, I'm ${this.name}`; +} + +const alice = { name: "Alice" }; + +// bind() returns a NEW function +const greetAlice = greet.bind(alice); + +// Call it whenever you want +greetAlice(); // "Hello, I'm Alice" +greetAlice(); // "Hello, I'm Alice" (still works!) +``` + +#### Key Characteristic: Permanent Binding + +Once bound, the `this` value cannot be changed, not even with `call()` or `apply()`: + +```javascript +function showThis() { + return this.name; +} + +const alice = { name: "Alice" }; +const bob = { name: "Bob" }; + +const boundToAlice = showThis.bind(alice); + +boundToAlice(); // "Alice" +boundToAlice.call(bob); // "Alice" (call ignored!) +boundToAlice.apply(bob); // "Alice" (apply ignored!) +boundToAlice.bind(bob)(); // "Alice" (bind ignored!) +``` + +#### Use Case: Event Handlers + +This is a common use of `bind()`: + +```javascript +class Toggle { + constructor() { + this.isOn = false; + + // Without bind, 'this' would be the button element + this.handleClick = this.handleClick.bind(this); + } + + handleClick() { + this.isOn = !this.isOn; + console.log(`Toggle is ${this.isOn ? 'ON' : 'OFF'}`); + } + + attachTo(button) { + button.addEventListener('click', this.handleClick); + } +} +``` + +#### Use Case: setTimeout and setInterval + +```javascript +class Countdown { + constructor(start) { + this.count = start; + } + + start() { + // Without bind, 'this' would be undefined in the callback + setInterval(this.tick.bind(this), 1000); + } + + tick() { + console.log(this.count--); + } +} + +const countdown = new Countdown(10); +countdown.start(); // 10, 9, 8, 7... +``` + +#### Use Case: Partial Application + +`bind()` can also pre-fill arguments, creating a specialized version of a function: + +```javascript +function multiply(a, b) { + return a * b; +} + +// Create specialized functions +const double = multiply.bind(null, 2); // 'a' is always 2 +const triple = multiply.bind(null, 3); // 'a' is always 3 + +double(5); // 10 (2 * 5) +triple(5); // 15 (3 * 5) +double(7); // 14 (2 * 7) +``` + +This technique is called **partial application**. You're partially applying arguments to create a more specific function. + +```javascript +function greet(greeting, name) { + return `${greeting}, ${name}!`; +} + +// Partial application: pre-fill the greeting +const sayHello = greet.bind(null, "Hello"); +const sayGoodbye = greet.bind(null, "Goodbye"); + +sayHello("Alice"); // "Hello, Alice!" +sayHello("Bob"); // "Hello, Bob!" +sayGoodbye("Alice"); // "Goodbye, Alice!" +``` + +--- + +## Common Patterns & Use Cases + +### Pattern 1: Method Borrowing + +Use array methods on array-like objects: + +```javascript +// Arguments object (old-school, but still seen in legacy code) +function sum() { + // 'arguments' is array-like but not an array (see MDN: Arguments object) + return Array.prototype.reduce.call( + arguments, + (total, n) => total + n, + 0 + ); +} + +sum(1, 2, 3, 4); // 10 + +// NodeList from DOM (browser-only example) +const divs = document.querySelectorAll('div'); // NodeList, not Array +const texts = Array.prototype.map.call(divs, div => div.textContent); + +// Modern alternative: Array.from() +const textsModern = Array.from(divs).map(div => div.textContent); +// Or spread +const textsSpread = [...divs].map(div => div.textContent); +``` + +### Pattern 2: Preserving Context in Classes + +The three main approaches to ensure `this` is correct in class methods: + +```javascript +class Player { + constructor(name) { + this.name = name; + this.score = 0; + + // Approach 1: Bind in constructor + this.incrementBound = this.incrementBound.bind(this); + } + + // Regular method - needs binding when used as callback + incrementBound() { + this.score++; + return this.score; + } + + // Approach 2: Arrow function class field + incrementArrow = () => { + this.score++; + return this.score; + } + + // Approach 3: Bind at call site (inline) + regularIncrement() { + this.score++; + return this.score; + } +} + +const player = new Player("Alice"); + +// All these work correctly: +setTimeout(player.incrementBound, 100); // Approach 1 +setTimeout(player.incrementArrow, 100); // Approach 2 +setTimeout(player.regularIncrement.bind(player), 100); // Approach 3 +setTimeout(() => player.regularIncrement(), 100); // Approach 3 alt +``` + +<Tip> +**Which approach is best?** + +- **Arrow class fields** (Approach 2) are the cleanest for most cases +- **Bind in constructor** (Approach 1) is useful when you need the method to also work as a regular method +- **Inline bind/arrow** (Approach 3) is fine for one-off uses but creates new functions each render in React +</Tip> + +### Pattern 3: Partial Application for Reusable Functions + +```javascript +// Generic logging function +function log(level, timestamp, message) { + console.log(`[${level}] ${timestamp}: ${message}`); +} + +// Create specialized loggers +const logError = log.bind(null, "ERROR"); +const logWarning = log.bind(null, "WARNING"); +const logInfo = log.bind(null, "INFO"); + +const now = new Date().toISOString(); + +logError(now, "Database connection failed"); +// [ERROR] 2024-01-15T10:30:00.000Z: Database connection failed + +logInfo(now, "Server started"); +// [INFO] 2024-01-15T10:30:00.000Z: Server started +``` + +--- + +## The Gotchas: Where `this` Goes Wrong + +<AccordionGroup> + <Accordion title="Gotcha 1: Lost Context in Callbacks"> + **The problem:** + ```javascript + class Timer { + constructor() { + this.seconds = 0; + } + + start() { + setInterval(function() { + this.seconds++; // ERROR: this is undefined! + console.log(this.seconds); + }, 1000); + } + } + ``` + + **Why it happens:** The callback function uses default binding, so `this` is `undefined` in strict mode. + + **Solutions:** + ```javascript + // Solution 1: Arrow function + start() { + setInterval(() => { + this.seconds++; // ✓ Arrow inherits 'this' + }, 1000); + } + + // Solution 2: bind() + start() { + setInterval(function() { + this.seconds++; // ✓ Bound to Timer instance + }.bind(this), 1000); + } + + // Solution 3: Store reference (old-school) + start() { + const self = this; + setInterval(function() { + self.seconds++; // ✓ Using closure + }, 1000); + } + ``` + </Accordion> + + <Accordion title="Gotcha 2: Extracting Methods from Objects"> + **The problem:** + ```javascript + const user = { + name: "Alice", + greet() { + return `Hi, I'm ${this.name}`; + } + }; + + const greet = user.greet; + greet(); // "Hi, I'm undefined" + ``` + + **Why it happens:** Assigning the method to a variable loses the implicit binding. + + **Solutions:** + ```javascript + // Solution 1: Keep as method call + user.greet(); // ✓ "Hi, I'm Alice" + + // Solution 2: Bind when extracting + const greet = user.greet.bind(user); + greet(); // ✓ "Hi, I'm Alice" + + // Solution 3: Wrapper function + const greet = () => user.greet(); + greet(); // ✓ "Hi, I'm Alice" + ``` + </Accordion> + + <Accordion title="Gotcha 3: Nested Functions Inside Methods"> + **The problem:** + ```javascript + const calculator = { + value: 0, + + add(numbers) { + numbers.forEach(function(n) { + this.value += n; // ERROR: this is undefined! + }); + return this.value; + } + }; + ``` + + **Why it happens:** The inner function has its own `this` (default binding), it doesn't inherit from `add()`. + + **Solutions:** + ```javascript + // Solution 1: Arrow function (recommended) + add(numbers) { + numbers.forEach((n) => { + this.value += n; // ✓ Arrow inherits 'this' + }); + return this.value; + } + + // Solution 2: Use thisArg parameter + add(numbers) { + numbers.forEach(function(n) { + this.value += n; // ✓ 'this' passed as second arg + }, this); + return this.value; + } + + // Solution 3: bind() + add(numbers) { + numbers.forEach(function(n) { + this.value += n; // ✓ Bound to calculator + }.bind(this)); + return this.value; + } + ``` + </Accordion> + + <Accordion title="Gotcha 4: Arrow Functions as Methods"> + **The problem:** + ```javascript + const user = { + name: "Alice", + greet: () => { + return `Hi, I'm ${this.name}`; // 'this' is NOT user! + } + }; + + user.greet(); // "Hi, I'm undefined" + ``` + + **Why it happens:** Arrow functions don't have their own `this`. The `this` here is from the surrounding scope (module/global), not `user`. + + **Solution:** Use regular functions for object methods: + ```javascript + const user = { + name: "Alice", + greet() { // Shorthand method syntax + return `Hi, I'm ${this.name}`; // ✓ this = user + } + }; + + user.greet(); // "Hi, I'm Alice" + ``` + </Accordion> +</AccordionGroup> + +--- + +## Arrow Functions: The Modern Solution + +Arrow functions were introduced in ES6 partly to solve `this` confusion. They work fundamentally differently. + +### How Arrow Functions Handle `this` + +1. **No own `this`**: Arrow functions don't create their own `this` binding +2. **Lexical inheritance**: They use `this` from the enclosing scope +3. **Permanent**: Cannot be changed by `call`, `apply`, or `bind` + +```javascript +const obj = { + name: "Object", + + regularMethod: function() { + console.log("Regular:", this.name); // "Object" + + // Nested regular function - loses 'this' + function inner() { + console.log("Inner regular:", this); // undefined + } + inner(); + + // Nested arrow function - keeps 'this' + const innerArrow = () => { + console.log("Inner arrow:", this.name); // "Object" + }; + innerArrow(); + } +}; +``` + +### When to Use Arrow Functions vs Regular Functions + +| Use Case | Arrow Function | Regular Function | +|----------|---------------|------------------| +| Object methods | ❌ No | ✅ Yes | +| Class methods (in prototype) | ❌ No | ✅ Yes | +| Callbacks needing outer `this` | ✅ Yes | ❌ No (needs bind) | +| Event handlers in classes | ✅ Yes (as class fields) | ⚠️ Needs binding | +| Functions needing own `this` | ❌ No | ✅ Yes | +| Constructor functions | ❌ No (can't use `new`) | ✅ Yes | +| Methods using [`arguments`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/arguments) | ❌ No (no `arguments`) | ✅ Yes | + +### Arrow Functions as Class Fields + +This is the most common pattern in modern JavaScript: + +```javascript +class SearchBox { + constructor(element) { + this.element = element; + this.query = ""; + + // Attach event listener - arrow function ensures correct 'this' + this.element.addEventListener('input', this.handleInput); + } + + // Arrow function as class field + handleInput = (event) => { + this.query = event.target.value; // 'this' is always SearchBox instance + this.performSearch(); + } + + performSearch = () => { + console.log(`Searching for: ${this.query}`); + } +} +``` + +### Limitations of Arrow Functions + +```javascript +// 1. Cannot be used with 'new' +const ArrowClass = () => {}; +new ArrowClass(); // TypeError: ArrowClass is not a constructor + +// 2. No own 'arguments' object +// Arrow functions inherit 'arguments' from enclosing function scope (if any) +function outer() { + const arrow = () => { + console.log(arguments); // Works! Uses outer's arguments + }; + arrow(); +} +outer(1, 2, 3); // logs [1, 2, 3] + +// But at module/global scope with no enclosing function: +const arrow = () => { + console.log(arguments); // ReferenceError: arguments is not defined +}; + +// Use rest parameters instead (recommended) +const arrowWithRest = (...args) => { + console.log(args); // Works everywhere! +}; + +// 3. No own 'super' binding (inherits from enclosing class method if any) + +// 4. Cannot be used as generators +// There's no arrow generator syntax - you must use function* +function* generatorFn() { yield 1; } // Works +// () =>* { yield 1; } // No such syntax exists +``` + +--- + +## Key Takeaways + +<Info> +**The key things to remember about `this`, `call`, `apply`, and `bind`:** + +1. **`this` is determined at call time** — Not when the function is defined, but when it's called. This is called dynamic binding. + +2. **5 binding rules in priority order** — new binding > explicit binding > implicit binding > default binding (arrow functions are special). + +3. **"Left of the dot" rule** — In method calls like `obj.method()`, `this` is the object immediately left of the dot. + +4. **Extracting methods loses `this`** — `const fn = obj.method; fn()` loses implicit binding. This is the #1 source of `this` bugs. + +5. **`call()` and `apply()` invoke immediately** — They set `this` and call the function right away. `call` takes comma-separated args, `apply` takes an array. + +6. **`bind()` returns a new function** — It permanently binds `this` for later use. The binding cannot be overridden, even with `call` or `apply`. + +7. **Arrow functions have no own `this`** — They inherit `this` from their enclosing scope (lexical binding). Perfect for callbacks. + +8. **Arrow functions can't be rebound** — `call`, `apply`, and `bind` have no effect on arrow functions' `this`. + +9. **Use arrow class fields for event handlers** — `handleClick = () => {}` ensures `this` is always the instance, even when extracted. + +10. **Strict mode changes default binding** — In strict mode, plain function calls have `this` as `undefined`, not the global object. +</Info> + +--- + +## Test Your Knowledge + +Try to figure out what `this` refers to in each example before revealing the answer. + +<AccordionGroup> + <Accordion title="Question 1: What does this log?"> + ```javascript + const user = { + name: "Alice", + greet() { + return `Hi, I'm ${this.name}`; + } + }; + + const greet = user.greet; + console.log(greet()); + ``` + + **Answer:** `"Hi, I'm undefined"` + + When `greet` is assigned to a variable and called without an object, implicit binding is lost. Default binding applies, and in strict mode `this` is `undefined`. + </Accordion> + + <Accordion title="Question 2: What does this log?"> + ```javascript + class Counter { + count = 0; + + increment = () => { + this.count++; + } + } + + const counter = new Counter(); + const inc = counter.increment; + inc(); + inc(); + console.log(counter.count); + ``` + + **Answer:** `2` + + Arrow function class fields have lexical `this` bound to the instance. Even when extracted, `this` still refers to `counter`. + </Accordion> + + <Accordion title="Question 3: What does this log?"> + ```javascript + function greet() { + return `Hello, ${this.name}!`; + } + + const alice = { name: "Alice" }; + const bob = { name: "Bob" }; + + const greetAlice = greet.bind(alice); + console.log(greetAlice.call(bob)); + ``` + + **Answer:** `"Hello, Alice!"` + + Once a function is bound with `bind()`, its `this` cannot be changed — not even with `call()`. The binding is permanent. + </Accordion> + + <Accordion title="Question 4: What does this log?"> + ```javascript + const obj = { + name: "Outer", + inner: { + name: "Inner", + getName() { + return this.name; + } + } + }; + + console.log(obj.inner.getName()); + ``` + + **Answer:** `"Inner"` + + With implicit binding, `this` is the object immediately to the left of the dot at call time. That's `obj.inner`, not `obj`. + </Accordion> + + <Accordion title="Question 5: What does this log?"> + ```javascript + const calculator = { + value: 10, + add(numbers) { + numbers.forEach(function(n) { + this.value += n; + }); + return this.value; + } + }; + + console.log(calculator.add([1, 2, 3])); + ``` + + **Answer:** `10` (and likely a TypeError in strict mode) + + The callback function inside `forEach` has its own `this` (default binding), which is `undefined` in strict mode. The fix is to use an arrow function: `numbers.forEach((n) => { this.value += n; })`. + </Accordion> + + <Accordion title="Question 6: What does this log?"> + ```javascript + function multiply(a, b) { + return a * b; + } + + const double = multiply.bind(null, 2); + console.log(double(5)); + console.log(double.length); + ``` + + **Answer:** `10` and `1` + + `bind` creates a partially applied function. `double(5)` returns `2 * 5 = 10`. The `length` property of a bound function reflects remaining parameters: `multiply` has 2 params, we pre-filled 1, so `double.length` is 1. + </Accordion> +</AccordionGroup> + +--- + +## Related Concepts + +<CardGroup cols={2}> + <Card title="Scope & Closures" icon="eye" href="/concepts/scope-and-closures"> + How variables are accessed — related to lexical this in arrow functions + </Card> + <Card title="Object Creation & Prototypes" icon="hammer" href="/concepts/object-creation-prototypes"> + How the new keyword creates objects and binds this + </Card> + <Card title="Factories and Classes" icon="industry" href="/concepts/factories-classes"> + Object creation patterns that rely on this binding + </Card> + <Card title="Inheritance & Polymorphism" icon="link" href="/concepts/inheritance-polymorphism"> + Understanding the prototype chain and method inheritance + </Card> +</CardGroup> + +--- + +## Reference + +<CardGroup cols={2}> + <Card title="this — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this"> + Official MDN documentation on the this keyword + </Card> + <Card title="call() — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call"> + MDN documentation for Function.prototype.call() + </Card> + <Card title="apply() — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply"> + MDN documentation for Function.prototype.apply() + </Card> + <Card title="bind() — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind"> + MDN documentation for Function.prototype.bind() + </Card> +</CardGroup> + +## Articles + +<CardGroup cols={2}> + <Card title="Grokking call(), apply() and bind() methods in JavaScript" icon="newspaper" href="https://levelup.gitconnected.com/grokking-call-apply-and-bind-methods-in-javascript-392351a4be8b"> + Uses a "borrowing a car" analogy that makes method borrowing click instantly. The side-by-side comparisons of call vs apply syntax are especially helpful. + </Card> + <Card title="Javascript: call(), apply() and bind()" icon="newspaper" href="https://medium.com/@omergoldberg/javascript-call-apply-and-bind-e5c27301f7bb"> + Builds understanding progressively from basic examples to implementing your own bind. Great for developers who want to know what's happening under the hood. + </Card> + + <Card title="The Top 7 Tricky this Interview Questions" icon="newspaper" href="https://dmitripavlutin.com/javascript-this-interview-questions/"> + Dmitri Pavlutin's collection of challenging this-related questions to test your understanding. + </Card> + <Card title="How to understand the keyword this and context in JavaScript" icon="newspaper" href="https://www.freecodecamp.org/news/how-to-understand-the-keyword-this-and-context-in-javascript-cd624c6b74b8/"> + Covers the relationship between execution context and `this` binding that many tutorials skip. The "3 scenarios" framework makes debugging `this` issues straightforward. + </Card> +</CardGroup> + +## Videos + +<CardGroup cols={2}> + <Card title="JavaScript call, apply and bind" icon="video" href="https://www.youtube.com/watch?v=c0mLRpw-9rI"> + Explains each method by solving real problems like borrowing array methods. The visual code walkthroughs make the execution order crystal clear. + </Card> + <Card title="JS Function Methods call(), apply(), and bind()" icon="video" href="https://www.youtube.com/watch?v=uBdH0iB1VDM"> + Shows exactly when `this` gets assigned during function execution. The step-through debugging demonstrations reveal what's actually happening in memory. + </Card> + <Card title="bind and this - Object Creation in JavaScript" icon="video" href="https://www.youtube.com/watch?v=GhbhD1HR5vk"> + MPJ's signature storytelling style makes `this` binding feel intuitive. Part of a larger series that builds up to understanding JavaScript's object system. + </Card> + <Card title="Javascript Interview Questions (Call, Bind and Apply)" icon="video" href="https://www.youtube.com/watch?v=VkmUOktYDAU"> + Roadside Coder's interview-focused video covering common questions about these methods. + </Card> +</CardGroup> diff --git a/docs/concepts/type-coercion.mdx b/docs/concepts/type-coercion.mdx new file mode 100644 index 00000000..e15656a9 --- /dev/null +++ b/docs/concepts/type-coercion.mdx @@ -0,0 +1,1023 @@ +--- +title: "Type Coercion: How Values Convert Automatically in JavaScript" +sidebarTitle: "Type Coercion: How Values Convert Automatically" +description: "Learn JavaScript type coercion and implicit conversion. Understand how values convert to strings, numbers, and booleans, the 8 falsy values, and how to avoid common coercion bugs." +--- + +Why does `"5" + 3` give you `"53"` but `"5" - 3` gives you `2`? Why does `[] == ![]` return `true`? How does JavaScript decide what type a value should be? + +```javascript +// JavaScript's "helpful" type conversion in action +console.log("5" + 3); // "53" (string concatenation!) +console.log("5" - 3); // 2 (numeric subtraction) +console.log([] == ![]); // true (wait, what?!) +``` + +This surprising behavior is **[type coercion](https://developer.mozilla.org/en-US/docs/Glossary/Type_coercion)**. JavaScript automatically converts values from one type to another. Understanding these rules helps you avoid bugs and write more predictable code. + +<Info> +**What you'll learn in this guide:** +- The difference between implicit and explicit coercion +- How JavaScript converts to strings, numbers, and booleans +- The 8 falsy values every developer must memorize +- How objects convert to primitives +- The famous JavaScript "WAT" moments explained +- Best practices for avoiding coercion bugs +</Info> + +<Warning> +**Prerequisites:** This guide assumes you understand [Primitive Types](/concepts/primitive-types). If terms like string, number, boolean, null, and undefined are new to you, read that guide first! +</Warning> + +--- + +## What Is Type Coercion? + +**Type coercion** is the automatic or implicit conversion of values from one data type to another in JavaScript. When you use operators or functions that expect a certain type, JavaScript will convert (coerce) values to make the operation work, sometimes helpfully, sometimes surprisingly. Understanding these conversion rules helps you write predictable, bug-free code. + +### The Shapeshifter Analogy + +Imagine JavaScript as an overly helpful translator. When you give it values of different types, it tries to "help" by converting them, sometimes correctly, sometimes... creatively. + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ THE OVERLY HELPFUL TRANSLATOR │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ YOU: "Hey JavaScript, add 5 and '3' together" │ +│ │ +│ JAVASCRIPT (thinking): "Hmm, one's a number, one's a string... │ +│ I'll just convert the number to a string! │ +│ '5' + '3' = '53'. You're welcome!" │ +│ │ +│ YOU: "That's... not what I meant." │ +│ │ +│ JAVASCRIPT: "¯\_(ツ)_/¯" │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +This "helpful" behavior is called **type coercion**. JavaScript automatically converts values from one type to another. Sometimes it's useful, sometimes it creates bugs that will haunt your dreams. + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ TYPE COERCION: THE SHAPESHIFTER │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────┐ ┌─────────┐ │ +│ │ "5" │ ──── + 3 ────────► │ "53" │ String won! │ +│ │ string │ │ string │ │ +│ └─────────┘ └─────────┘ │ +│ │ +│ ┌─────────┐ ┌─────────┐ │ +│ │ "5" │ ──── - 3 ────────► │ 2 │ Number won! │ +│ │ string │ │ number │ │ +│ └─────────┘ └─────────┘ │ +│ │ +│ Same values, different operators, different results! │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### Explicit vs Implicit Coercion + +There are two ways coercion happens: + +<Tabs> + <Tab title="Explicit Coercion"> + **You** control the conversion using built-in functions. This is predictable and intentional. + + ```javascript + // YOU decide when and how to convert + Number("42") // 42 + String(42) // "42" + Boolean(1) // true + + parseInt("42px") // 42 + parseFloat("3.14") // 3.14 + ``` + + These functions — [`Number()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number), [`String()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String), [`Boolean()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean), [`parseInt()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseInt), and [`parseFloat()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseFloat) — give you full control. + + This is the **safe** way. You know exactly what's happening. + </Tab> + <Tab title="Implicit Coercion"> + **JavaScript** automatically converts types when operators or functions expect a different type. + + ```javascript + // JavaScript "helps" without asking + "5" + 3 // "53" (number became string) + "5" - 3 // 2 (string became number) + + if ("hello") {} // string became boolean (true) + + 5 == "5" // true (types were coerced) + ``` + + This is where bugs hide. Learn the rules or suffer the consequences! + </Tab> +</Tabs> + +### Why Does JavaScript Do This? + +JavaScript is a **dynamically typed** language. Variables don't have fixed types. This flexibility means JavaScript needs to figure out what to do when types don't match. + +```javascript +// In JavaScript, variables can hold any type +let x = 42; // x is a number +x = "hello"; // now x is a string +x = true; // now x is a boolean + +// So what happens here? +let result = x + 10; // JavaScript must decide how to handle this +``` + +Other languages would throw an error. JavaScript tries to make it work. Whether that's a feature or a bug... depends on who you ask! + +--- + +## The Three Types of Conversion + +Here's the most important rule: **JavaScript can only convert to THREE [primitive types](/concepts/primitive-types):** + +| Target Type | Explicit Method | Common Implicit Triggers | +|-------------|-----------------|--------------------------| +| **String** | `String(value)` | `+` with a string, template literals | +| **Number** | `Number(value)` | Math operators (`- * / %`), comparisons | +| **Boolean** | `Boolean(value)` | `if`, `while`, `!`, `&&`, `\|\|`, `? :` | + +That's it. No matter how complex the coercion seems, the end result is always a string, number, or boolean. + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ THE THREE CONVERSION DESTINATIONS │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────────────────┐ │ +│ │ ANY VALUE │ │ +│ │ (string, number, │ │ +│ │ object, array...) │ │ +│ └──────────┬───────────┘ │ +│ │ │ +│ ┌────────────────┼────────────────┐ │ +│ ▼ ▼ ▼ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ String │ │ Number │ │ Boolean │ │ +│ │ "42" │ │ 42 │ │ true │ │ +│ └──────────┘ └──────────┘ └──────────┘ │ +│ │ +│ These are the ONLY three possible destinations! │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## String Conversion + +String conversion is the most straightforward. Almost anything can become a string. + +### When Does It Happen? + +```javascript +// Explicit conversion +String(123) // "123" +String(true) // "true" +(123).toString() // "123" + +// Implicit conversion +123 + "" // "123" (concatenation with empty string) +`Value: ${123}` // "Value: 123" (template literal) +"Hello " + 123 // "Hello 123" (+ with a string) +``` + +The [`toString()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString) method and [template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals) are also common ways to convert values to strings. + +### String Conversion Rules + +| Value | Result | Notes | +|-------|--------|-------| +| `123` | `"123"` | Numbers become digit strings | +| `-12.34` | `"-12.34"` | Decimals and negatives work too | +| `true` | `"true"` | Booleans become their word | +| `false` | `"false"` | | +| `null` | `"null"` | | +| `undefined` | `"undefined"` | | +| `[1, 2, 3]` | `"1,2,3"` | Arrays join with commas | +| `[]` | `""` | Empty array becomes empty string | +| `{}` | `"[object Object]"` | Objects become this (usually useless) | +| [`Symbol("id")`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol) | Throws TypeError! | Symbols can't implicitly convert | + +### The + Operator's Split Personality + +The `+` operator is special: it does **both** addition and concatenation: + +```javascript +// With two numbers: addition +5 + 3 // 8 + +// With any string involved: concatenation +"5" + 3 // "53" (3 becomes "3") +5 + "3" // "53" (5 becomes "5") +"Hello" + " World" // "Hello World" + +// Order matters with multiple operands! +1 + 2 + "3" // "33" (1+2=3, then 3+"3"="33") +"1" + 2 + 3 // "123" (all become strings left-to-right) +``` + +<Warning> +**Common gotcha:** The `+` operator with strings catches many developers off guard. If you're doing math and get unexpected string concatenation, check if any value might be a string! + +```javascript +// Dangerous: user input is always a string! +const userInput = "5"; +const result = userInput + 10; // "510", not 15! + +// Safe: convert first +const result = Number(userInput) + 10; // 15 +``` +</Warning> + +--- + +## Number Conversion + +Number conversion has more triggers than string conversion, and more edge cases to memorize. + +### When Does It Happen? + +```javascript +// Explicit conversion +Number("42") // 42 +parseInt("42px") // 42 (stops at non-digit) +parseFloat("3.14") // 3.14 ++"42" // 42 (unary plus trick) + +// Implicit conversion +"6" - 2 // 4 (subtraction) +"6" * 2 // 12 (multiplication) +"6" / 2 // 3 (division) +"6" % 4 // 2 (modulo) +"10" > 5 // true (comparison) ++"42" // 42 (unary plus) +``` + +### Number Conversion Rules + +| Value | Result | Notes | +|-------|--------|-------| +| `"123"` | `123` | Numeric strings work | +| `" 123 "` | `123` | Whitespace is trimmed | +| `"123abc"` | [`NaN`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NaN) | Any non-numeric char → NaN | +| `""` | `0` | Empty string becomes 0 | +| `" "` | `0` | Whitespace-only becomes 0 | +| `true` | `1` | | +| `false` | `0` | | +| `null` | `0` | null → 0 | +| `undefined` | `NaN` | undefined → NaN (different!) | +| `[]` | `0` | Empty array → "" → 0 | +| `[1]` | `1` | Single element array | +| `[1, 2]` | `NaN` | Multiple elements → NaN | +| `{}` | `NaN` | Objects → NaN | + +<Warning> +**null vs undefined:** Notice that `Number(null)` is `0` but `Number(undefined)` is `NaN`. This inconsistency trips up many developers! + +```javascript +Number(null) // 0 +Number(undefined) // NaN + +null + 5 // 5 +undefined + 5 // NaN +``` +</Warning> + +### Math Operators Always Convert to Numbers + +Unlike `+`, the other math operators (`-`, `*`, `/`, `%`) **only** do math. They always convert to numbers: + +```javascript +"6" - "2" // 4 (both become numbers) +"6" * "2" // 12 +"6" / "2" // 3 +"10" % "3" // 1 + +// This is why - and + behave differently! +"5" + 3 // "53" (concatenation) +"5" - 3 // 2 (math) +``` + +### The Unary + Trick + +The unary `+` (plus sign before a value) is a quick way to convert to a number: + +```javascript ++"42" // 42 ++true // 1 ++false // 0 ++null // 0 ++undefined // NaN ++"hello" // NaN ++"" // 0 +``` + +--- + +## Boolean Conversion + +Boolean conversion is actually the simplest. Every value is either **truthy** or **falsy**. + +### When Does It Happen? + +```javascript +// Explicit conversion +Boolean(1) // true +Boolean(0) // false +!!value // double negation trick + +// Implicit conversion +if (value) { } // condition check +while (value) { } // loop condition +value ? "yes" : "no" // ternary operator +value && doSomething() // logical AND +value || defaultValue // logical OR +!value // logical NOT +``` + +### The 8 Falsy Values (Memorize These!) + +There are **8 common values** that convert to `false`. Everything else is `true`. + +```javascript +// THE FALSY EIGHT +Boolean(false) // false (obviously) +Boolean(0) // false +Boolean(-0) // false (yes, -0 exists) +Boolean(0n) // false ([BigInt](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt) zero) +Boolean("") // false (empty string) +Boolean(null) // false +Boolean(undefined) // false +Boolean(NaN) // false +``` + +<Info> +**Technical note:** There's actually a 9th falsy value: [`document.all`](https://developer.mozilla.org/en-US/docs/Web/API/Document/all). It's a legacy browser API that returns `false` in boolean context despite being an object. You'll rarely encounter it in modern code, but it exists for backwards compatibility with ancient websites. +</Info> + +### Everything Else Is Truthy! + +This includes some surprises: + +```javascript +// These are all TRUE! +Boolean(true) // true (obviously) +Boolean(1) // true +Boolean(-1) // true (negative numbers!) +Boolean("hello") // true +Boolean("0") // true (non-empty string!) +Boolean("false") // true (non-empty string!) +Boolean([]) // true (empty array!) +Boolean({}) // true (empty object!) +Boolean(function(){}) // true +Boolean(new Date()) // true +Boolean(Infinity) // true +Boolean(-Infinity) // true +``` + +<Warning> +**Common gotchas:** + +```javascript +// These catch people ALL the time: +Boolean("0") // true (it's a non-empty string!) +Boolean("false") // true (it's a non-empty string!) +Boolean([]) // true (arrays are objects, objects are truthy) +Boolean({}) // true (even empty objects) + +// If checking for empty array, do this: +if (arr.length) { } // checks if array has items +if (arr.length === 0) { } // checks if array is empty +``` +</Warning> + +### Logical Operators Don't Return Booleans! + +A common misconception: `&&` and `||` don't necessarily return `true` or `false`. They return one of the **original values**: + +```javascript +// || returns the FIRST truthy value (or the last value) +"hello" || "world" // "hello" +"" || "world" // "world" +"" || 0 || null || "yes" // "yes" + +// && returns the FIRST falsy value (or the last value) +"hello" && "world" // "world" +"" && "world" // "" +1 && 2 && 3 // 3 + +// This is useful for defaults! +const name = userInput || "Anonymous"; +const display = user && user.name; +``` + +--- + +## Object to Primitive Conversion + +When JavaScript needs to convert an [object to a primitive](/concepts/value-reference-types) (including arrays), it follows a specific algorithm. + +### The ToPrimitive Algorithm + +<Steps> + <Step title="Check if already primitive"> + If the value is already a primitive (string, number, boolean, etc.), return it as-is. + </Step> + + <Step title="Determine the 'hint'"> + JavaScript decides whether it wants a "string" or "number" based on context: + - **String hint:** `String()`, template literals, property keys + - **Number hint:** `Number()`, math operators, comparisons + - **Default hint:** `+` operator, `==` (usually treated as number) + </Step> + + <Step title="Try valueOf() or toString()"> + - For **number** hint: try `valueOf()` first, then `toString()` + - For **string** hint: try `toString()` first, then `valueOf()` + </Step> + + <Step title="Return primitive or throw"> + If a primitive is returned, use it. Otherwise, throw `TypeError`. + </Step> +</Steps> + +### How Built-in Objects Convert + +```javascript +// Arrays - toString() returns joined elements +[1, 2, 3].toString() // "1,2,3" +[1, 2, 3] + "" // "1,2,3" +[1, 2, 3] - 0 // NaN (can't convert "1,2,3" to number) + +[].toString() // "" +[] + "" // "" +[] - 0 // 0 (empty string → 0) + +[1].toString() // "1" +[1] - 0 // 1 + +// Plain objects - toString() returns "[object Object]" +({}).toString() // "[object Object]" +({}) + "" // "[object Object]" + +// Dates - special case, prefers string for + operator +const date = new Date(0); +date.toString() // "Thu Jan 01 1970 ..." +date.valueOf() // 0 (timestamp in ms) + +date + "" // "Thu Jan 01 1970 ..." (uses toString) +date - 0 // 0 (uses valueOf) +``` + +### Custom Conversion with valueOf and toString + +You can control how your objects convert: + +```javascript +const price = { + amount: 99.99, + currency: "USD", + + valueOf() { + return this.amount; + }, + + toString() { + return `${this.currency} ${this.amount}`; + } +}; + +// Number conversion uses valueOf() +price - 0 // 99.99 +price * 2 // 199.98 ++price // 99.99 + +// String conversion uses toString() +String(price) // "USD 99.99" +`Price: ${price}` // "Price: USD 99.99" + +// + is ambiguous, uses valueOf() if it returns primitive +price + "" // "99.99" (valueOf returned number, then → string) +``` + +### ES6 Symbol.toPrimitive + +ES6 introduced a cleaner way to control conversion — [`Symbol.toPrimitive`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toPrimitive): + +```javascript +const obj = { + [Symbol.toPrimitive](hint) { + console.log(`Converting with hint: ${hint}`); + + if (hint === "number") { + return 42; + } + if (hint === "string") { + return "forty-two"; + } + // hint === "default" + return "default value"; + } +}; + ++obj // 42 (hint: "number") +`${obj}` // "forty-two" (hint: "string") +obj + "" // "default value" (hint: "default") +``` + +--- + +## The == Algorithm Explained + +The loose equality operator `==` is where type coercion gets wild. For a deeper dive into all equality operators, see our [Equality Operators guide](/concepts/equality-operators). Here's how `==` actually works: + +### Simplified == Rules + +<Steps> + <Step title="Same type?"> + Compare directly (like `===`). + ```javascript + 5 == 5 // true + "hello" == "hello" // true + ``` + </Step> + + <Step title="null or undefined?"> + `null == undefined` is `true`. Neither equals anything else. + ```javascript + null == undefined // true + null == null // true + null == 0 // false (special rule!) + null == "" // false + ``` + </Step> + + <Step title="Number vs String?"> + Convert the string to a number. + ```javascript + 5 == "5" + // becomes: 5 == 5 + // result: true + ``` + </Step> + + <Step title="Boolean involved?"> + Convert the boolean to a number FIRST. + ```javascript + true == "1" + // step 1: 1 == "1" (true → 1) + // step 2: 1 == 1 (string → number) + // result: true + + true == "true" + // step 1: 1 == "true" (true → 1) + // step 2: 1 == NaN ("true" → NaN) + // result: false (surprise!) + ``` + </Step> + + <Step title="Object vs Primitive?"> + Convert the object to a primitive. + ```javascript + [1] == 1 + // step 1: "1" == 1 (array → string "1") + // step 2: 1 == 1 (string → number) + // result: true + ``` + </Step> +</Steps> + +### Step-by-Step Examples + +```javascript +// Example 1: "5" == 5 +"5" == 5 +// String vs Number → convert string to number +// 5 == 5 +// Result: true + +// Example 2: true == "1" +true == "1" +// Boolean involved → convert boolean to number first +// 1 == "1" +// Number vs String → convert string to number +// 1 == 1 +// Result: true + +// Example 3: [] == false +[] == false +// Boolean involved → convert boolean to number first +// [] == 0 +// Object vs Number → convert object to primitive +// "" == 0 (empty array → empty string) +// String vs Number → convert string to number +// 0 == 0 +// Result: true + +// Example 4: [] == ![] +[] == ![] +// First, evaluate ![] → false (arrays are truthy) +// [] == false +// Boolean involved → false becomes 0 +// [] == 0 +// Object vs Number → [] becomes "" +// "" == 0 +// String vs Number → "" becomes 0 +// 0 == 0 +// Result: true (yes, really!) +``` + +<Tip> +**Just use `===`!** The triple equals operator never coerces types. If the types are different, it returns `false` immediately. This is almost always what you want. + +```javascript +5 === "5" // false (different types) +5 == "5" // true (coerced) + +null === undefined // false +null == undefined // true +``` +</Tip> + +--- + +## Operators & Coercion Cheat Sheet + +Quick reference for which operators trigger which coercion: + +| Operator | Coercion Type | Example | Result | +|----------|---------------|---------|--------| +| `+` (with string) | String | `"5" + 3` | `"53"` | +| `+` (unary) | Number | `+"5"` | `5` | +| `-` `*` `/` `%` | Number | `"5" - 3` | `2` | +| `++` `--` | Number | `let x = "5"; x++` | `6` | +| `>` `<` `>=` `<=` | Number | `"10" > 5` | `true` | +| `==` `!=` | Complex | `"5" == 5` | `true` | +| `===` `!==` | None | `"5" === 5` | `false` | +| `&&` `\|\|` | Boolean (internal) | `"hi" \|\| "bye"` | `"hi"` | +| `!` | Boolean | `!"hello"` | `false` | +| `if` `while` `? :` | Boolean | `if ("hello")` | `true` | +| `&` `\|` `^` `~` | Number (32-bit int) | `"5" \| 0` | `5` | + +--- + +## JavaScript WAT Moments + +Let's explore the famous "weird parts" that make JavaScript... special. + +<AccordionGroup> + <Accordion title="1. The + Operator's Split Personality"> + ```javascript + "5" + 3 // "53" (string concatenation) + "5" - 3 // 2 (math!) + + // Why? + does both addition AND concatenation + // If either operand is a string, it concatenates + // - only does subtraction, so it converts to numbers + ``` + </Accordion> + + <Accordion title="2. Empty Array Weirdness"> + ```javascript + [] + [] // "" + // Both arrays → "", then "" + "" = "" + + [] + {} // "[object Object]" + // [] → "", {} → "[object Object]" + + {} + [] // 0 (in browser console!) + // {} is parsed as empty block, then +[] = 0 + // Wrap in parens to fix: ({}) + [] = "[object Object]" + ``` + </Accordion> + + <Accordion title="3. Boolean Math"> + ```javascript + true + true // 2 (1 + 1) + true + false // 1 (1 + 0) + true - true // 0 (1 - 1) + + // Booleans convert to 1 (true) or 0 (false) + ``` + </Accordion> + + <Accordion title="4. The Infamous [] == ![]"> + ```javascript + [] == ![] // true + + // Step by step: + // 1. ![] → false (arrays are truthy, negated = false) + // 2. [] == false + // 3. [] == 0 (boolean → number) + // 4. "" == 0 (array → string) + // 5. 0 == 0 (string → number) + // 6. true! + + // Meanwhile... + [] === ![] // false (different types, no coercion) + ``` + </Accordion> + + <Accordion title='5. "foo" + + "bar"'> + ```javascript + "foo" + + "bar" // "fooNaN" + + // Step by step: + // 1. +"bar" is evaluated first (unary +) + // 2. +"bar" → NaN (can't convert "bar" to number) + // 3. "foo" + NaN → "fooNaN" + ``` + </Accordion> + + <Accordion title="6. NaN is Not Equal to Itself"> + ```javascript + NaN === NaN // false + NaN == NaN // false + + // NaN is the only value in JavaScript not equal to itself! + // This is by design (IEEE 754 spec) + + // How to check for NaN: + Number.isNaN(NaN) // true (correct way) + isNaN(NaN) // true + isNaN("hello") // true (wrong! it converts first) + Number.isNaN("hello") // false (correct) + + ``` + + Use [`Number.isNaN()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isNaN) instead of the global [`isNaN()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/isNaN) for reliable NaN checking. + </Accordion> + + <Accordion title="7. typeof Quirks"> + ```javascript + typeof NaN // "number" (wat) + typeof null // "object" (historical bug) + typeof [] // "object" (arrays are objects) + typeof function(){} // "function" (special case) + ``` + </Accordion> + + <Accordion title="8. Adding Arrays"> + ```javascript + [1, 2] + [3, 4] // "1,23,4" + + // Arrays convert to strings: + // [1, 2] → "1,2" + // [3, 4] → "3,4" + // "1,2" + "3,4" → "1,23,4" + + // To actually combine arrays: + [...[1, 2], ...[3, 4]] // [1, 2, 3, 4] + [1, 2].concat([3, 4]) // [1, 2, 3, 4] + ``` + </Accordion> +</AccordionGroup> + +--- + +## Best Practices + +<Tip> +**How to avoid coercion bugs:** + +1. **Use `===` instead of `==`** — No surprises, no coercion +2. **Be explicit** — Use `Number()`, `String()`, `Boolean()` when converting +3. **Validate input** — Don't assume types, especially from user input +4. **Use `Number.isNaN()`** — Not `isNaN()` or `=== NaN` +5. **Be careful with `+`** — Remember it concatenates if any operand is a string +</Tip> + +### When Implicit Coercion IS Useful + +Despite the gotchas, some implicit coercion patterns are actually helpful: + +```javascript +// 1. Checking for null OR undefined in one shot +if (value == null) { + // This catches BOTH null and undefined + // Much cleaner than: if (value === null || value === undefined) +} + +// 2. Boolean context is natural and readable +if (user) { + // Truthy check - totally fine +} + +if (items.length) { + // Checking if array has items - totally fine +} + +// 3. Quick string conversion +const str = value + ""; +// or +const str = String(value); +// or +const str = `${value}`; + +// 4. Quick number conversion +const num = +value; +// or +const num = Number(value); +``` + +### Anti-Patterns to Avoid + +```javascript +// BAD: Relying on == for type-unsafe comparisons +if (x == true) { } // Don't do this! +if (x) { } // Do this instead + +// BAD: Using == with 0 or "" +if (x == 0) { } // Matches "", but not null (null == 0 is false!) +if (x === 0) { } // Clear intent + +// BAD: Truthy check when you need specific type +function process(count) { + if (!count) return; // Fails for count = 0! + // ... +} + +function process(count) { + if (typeof count !== "number") return; // Better + // ... +} +``` + +--- + +## Key Takeaways + +<Info> +**The key things to remember about Type Coercion:** + +1. **Three conversions only** — JavaScript converts to String, Number, or Boolean — nothing else + +2. **Implicit vs Explicit** — Know when JS converts automatically vs when you control it + +3. **The 8 common falsy values** — `false`, `0`, `-0`, `0n`, `""`, `null`, `undefined`, `NaN` — everything else is truthy (plus the rare `document.all`) + +4. **+ is special** — It prefers string concatenation if ANY operand is a string + +5. **- * / % are consistent** — They ALWAYS convert to numbers + +6. **== coerces, === doesn't** — Use `===` by default to avoid surprises + +7. **null == undefined** — This is true, but neither equals anything else with `==` + +8. **Objects convert via valueOf() and toString()** — Learn these methods to control conversion + +9. **When in doubt, be explicit** — Use `Number()`, `String()`, `Boolean()` + +10. **NaN is unique** — It's the only value not equal to itself; use `Number.isNaN()` to check +</Info> + +--- + +## Test Your Knowledge + +<AccordionGroup> + <Accordion title='Question 1: What does "5" + 3 return and why?'> + **Answer:** `"53"` (string) + + The `+` operator, when one operand is a string, performs string concatenation. The number `3` is converted to `"3"`, resulting in `"5" + "3" = "53"`. + </Accordion> + + <Accordion title="Question 2: What are the 8 common falsy values in JavaScript?"> + **Answer:** + 1. `false` + 2. `0` + 3. `-0` + 4. `0n` (BigInt zero) + 5. `""` (empty string) + 6. `null` + 7. `undefined` + 8. `NaN` + + Everything else is truthy, including `[]`, `{}`, `"0"`, and `"false"`. + + **Bonus:** There's also a 9th falsy value — `document.all` — a legacy browser API you'll rarely encounter. + </Accordion> + + <Accordion title="Question 3: Why does [] == ![] return true?"> + **Answer:** This is a multi-step coercion: + + 1. `![]` evaluates first: arrays are truthy, so `![]` = `false` + 2. Now we have `[] == false` + 3. Boolean converts to number: `[] == 0` + 4. Array converts to primitive: `"" == 0` + 5. String converts to number: `0 == 0` + 6. Result: `true` + </Accordion> + + <Accordion title="Question 4: What's the difference between == and === regarding coercion?"> + **Answer:** + + - `===` (strict equality) **never** coerces. If types differ, it returns `false` immediately. + - `==` (loose equality) **coerces** values to the same type before comparing, following a complex algorithm. + + ```javascript + 5 === "5" // false (different types) + 5 == "5" // true (string coerced to number) + ``` + + Best practice: Use `===` unless you specifically need coercion. + </Accordion> + + <Accordion title="Question 5: What does Number(null) vs Number(undefined) return?"> + **Answer:** + + ```javascript + Number(null) // 0 + Number(undefined) // NaN + ``` + + This inconsistency is a common source of bugs. `null` converts to `0` (like "nothing" = zero), while `undefined` converts to `NaN` (like "no value" = not a number). + </Accordion> + + <Accordion title='Question 6: Predict the output: true + false + "hello"'> + **Answer:** `"1hello"` + + Step by step: + 1. `true + false` = `1 + 0` = `1` (booleans → numbers) + 2. `1 + "hello"` = `"1hello"` (number → string for concatenation) + </Accordion> +</AccordionGroup> + +--- + +## Related Concepts + +<CardGroup cols={2}> + <Card title="Primitive Types" icon="atom" href="/concepts/primitive-types"> + Understanding the basic data types that coercion converts between + </Card> + <Card title="Value Types vs Reference Types" icon="clone" href="/concepts/value-reference-types"> + How primitives and objects behave differently during coercion + </Card> + <Card title="Equality Operators" icon="equals" href="/concepts/equality-operators"> + Deep dive into ==, ===, and how coercion affects comparisons + </Card> + <Card title="JavaScript Engines" icon="gear" href="/concepts/javascript-engines"> + How engines like V8 implement type coercion internally + </Card> +</CardGroup> + +--- + +## Reference + +<CardGroup cols={2}> + <Card title="Type Coercion — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Glossary/Type_coercion"> + Official MDN glossary entry explaining type coercion fundamentals. + </Card> + <Card title="Equality Comparisons — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness"> + Comprehensive guide to ==, ===, Object.is() and the coercion rules behind each. + </Card> + <Card title="Type Conversion — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Glossary/Type_Conversion"> + The difference between type coercion (implicit) and type conversion (explicit). + </Card> + <Card title="Truthy and Falsy — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Glossary/Falsy"> + Complete list of falsy values and how boolean context works. + </Card> +</CardGroup> + +## Articles + +<CardGroup cols={2}> + <Card title="JavaScript Type Coercion Explained" icon="newspaper" href="https://medium.freecodecamp.org/js-type-coercion-explained-27ba3d9a2839"> + Comprehensive freeCodeCamp article by Alexey Samoshkin covering all coercion rules with tons of examples and quiz questions. One of the best resources available. + </Card> + <Card title="What you need to know about Javascript's Implicit Coercion" icon="newspaper" href="https://dev.to/promhize/what-you-need-to-know-about-javascripts-implicit-coercion-e23"> + Practical guide by Promise Tochi covering implicit coercion patterns, valueOf/toString, and common gotchas with clear examples. + </Card> + <Card title="Object to Primitive Conversion" icon="newspaper" href="https://javascript.info/object-toprimitive"> + Deep-dive from javascript.info into how objects convert to primitives using Symbol.toPrimitive, toString, and valueOf. Essential for advanced understanding. + </Card> + <Card title="You Don't Know JS: Types & Grammar, Ch. 4" icon="newspaper" href="https://github.com/getify/You-Dont-Know-JS/blob/2nd-ed/types-grammar/ch4.md"> + Kyle Simpson's definitive deep-dive into JavaScript coercion. Explains abstract operations, ToString, ToNumber, ToBoolean, and the "why" behind every rule. Free to read online. + </Card> +</CardGroup> + +## Videos + +<CardGroup cols={2}> + <Card title="== ? === ??? ...#@^% — JSConf EU" icon="video" href="https://www.youtube.com/watch?v=qGyqzN0bjhc"> + Entertaining JSConf talk by Shirmung Bielefeld exploring the chaos of JavaScript equality operators with live examples and audience participation. + </Card> + <Card title="Coercion in Javascript — Hitesh Choudhary" icon="video" href="https://www.youtube.com/watch?v=b04Q_vyqEG8"> + Hitesh walks through coercion step-by-step in the browser console, showing exactly what JavaScript does at each conversion. Good pace for beginners. + </Card> + <Card title="What is Coercion? — Steven Hancock" icon="video" href="https://www.youtube.com/watch?v=z4-8wMSPJyI"> + Steven breaks down the three conversion types (string, number, boolean) with simple examples. Short video that covers the fundamentals quickly. + </Card> +</CardGroup> diff --git a/docs/concepts/value-reference-types.mdx b/docs/concepts/value-reference-types.mdx new file mode 100644 index 00000000..5d9d1f29 --- /dev/null +++ b/docs/concepts/value-reference-types.mdx @@ -0,0 +1,1288 @@ +--- +title: "Value vs Reference Types: How Memory Works in JavaScript" +sidebarTitle: "Value vs Reference Types: How Memory Works" +description: "Learn how value types and reference types work in JavaScript. Understand how primitives and objects are stored, why copying objects shares references, and how to avoid mutation bugs." +--- + +Have you ever wondered why changing one variable unexpectedly changes another? Why does this happen? + +```javascript +const original = { name: "Alice" }; +const copy = original; +copy.name = "Bob"; + +console.log(original.name); // "Bob" — Wait, what?! +``` + +The answer lies in how JavaScript stores data in memory. **Primitives** (like numbers and strings) store actual values, while **objects** store *references* (pointers) to data. This difference causes countless bugs in JavaScript code. + +<Info> +**What you'll learn in this guide:** +- How JavaScript stores primitives vs objects in memory +- Why copying an object doesn't create a new object +- The difference between "pass by value" and "pass by reference" +- Why `{} === {}` returns `false` +- How to properly clone objects (shallow vs deep copy) +- Common bugs caused by reference sharing +</Info> + +<Warning> +**Prerequisite:** This guide assumes you understand [Primitive Types](/concepts/primitive-types). If you're not familiar with the 7 primitive types in JavaScript, read that guide first! +</Warning> + +--- + +## What Are Value Types and Reference Types? + +JavaScript has two categories of data types that behave very differently: + +### Value Types (Primitives) + +**The 7 primitive types** store their values directly: + +| Type | Example | Stored As | +|------|---------|-----------| +| `string` | `"hello"` | The string value | +| `number` | `42` | The numeric value | +| `bigint` | `9007199254740993n` | The large integer value | +| `boolean` | `true` | The boolean value | +| `undefined` | `undefined` | The undefined value | +| `null` | `null` | The null value | +| [`symbol`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol) | `Symbol("id")` | The unique symbol | + +**Key characteristics:** +- Behave as if stored directly in the variable +- Immutable — you can't change them, only replace them +- Copied by value — copies are independent +- Compared by value — same value = equal + +<Info> +**Under the hood:** Modern JavaScript engines optimize string storage through a technique called "string interning" — identical strings may share the same memory location internally. However, this is an optimization detail; strings still *behave* as independent values when you work with them. +</Info> + +### Reference Types + +**Everything else** is a reference type: + +| Type | Example | Stored As | +|------|---------|-----------| +| Object | `{ name: "Alice" }` | Reference to object | +| Array | `[1, 2, 3]` | Reference to array | +| Function | `function() {}` | Reference to function | +| Date | `new Date()` | Reference to date | +| RegExp | `/pattern/` | Reference to regex | +| [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) | `new Map()` | Reference to map | +| [Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) | `new Set()` | Reference to set | + +**Key characteristics:** +- Variable stores a *reference* (pointer) to the actual data +- Mutable — you CAN change their contents +- Copied by reference — copies point to the SAME object +- Compared by reference — same reference = equal (not same contents!) + +--- + +## The Sticky Note vs The Map: A Real-World Analogy + +Imagine you have two ways to share information with a friend: + +**Sticky Note (Value Types):** You write "42" on a sticky note and hand it to your friend. They now have their own note with "42" on it. If they change their note to "100", your note still says "42". You each have independent copies. + +**Map to Treasure (Reference Types):** Instead of giving your friend the treasure itself, you give them a map to where the treasure is buried. Now you BOTH have maps pointing to the SAME treasure. If they dig it up and add more gold, you'll see the extra gold too, because you're both looking at the same treasure! + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ VALUE TYPES vs REFERENCE TYPES │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ VALUE TYPE (Sticky Note) REFERENCE TYPE (Map to Treasure) │ +│ │ +│ ┌─────────────┐ ┌─────────────┐ │ +│ │ x = 42 │ │ x = ────────────────┐ │ +│ └─────────────┘ └─────────────┘ │ │ +│ ▼ │ +│ ┌─────────────┐ ┌─────────────┐ ┌──────────┐ │ +│ │ y = 42 │ (independent copy) │ y = ────────────►│ {...} │ │ +│ └─────────────┘ └─────────────┘ └──────────┘ │ +│ │ +│ Change y? Change the object via y? │ +│ x stays the same! x sees the change too! │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +This difference between "storing the value itself" vs "storing a map to the value" is fundamental to understanding JavaScript. + +<Tip> +**Quick Rule:** Primitives store the actual value. Copying creates an independent copy. Objects and arrays store a *reference* (pointer). Copying creates another pointer to the SAME data. +</Tip> + +--- + +## How Memory Works: Stack vs Heap + +To truly understand the difference, you need to know where JavaScript stores data. + +<Info> +**Important note:** The stack/heap model described below is a **conceptual simplification** to help you understand how value types and reference types *behave*. The JavaScript specification doesn't define where values are stored in memory—actual engines (V8, SpiderMonkey, etc.) may optimize storage differently. What matters is understanding the *behavior* difference, not the physical memory location. +</Info> + +### The Stack: Home of Primitives + +The **stack** is a fast, organized region of memory. It stores: +- Primitive values +- References (pointers) to objects +- Function call information + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ THE STACK │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌────────────────────────────┐ │ +│ │ name = "Alice" │ ← Actual string value │ +│ ├────────────────────────────┤ │ +│ │ age = 25 │ ← Actual number value │ +│ ├────────────────────────────┤ │ +│ │ isActive = true │ ← Actual boolean value │ +│ ├────────────────────────────┤ │ +│ │ user = 0x7F3A ──────┼──── Points to heap │ +│ └────────────────────────────┘ │ +│ │ +│ Fixed size, fast access │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### The Heap: Home of Objects + +The **heap** is a larger, less organized region for dynamic data: +- Objects +- Arrays +- Functions +- Anything that can grow or change size + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ THE HEAP │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────┐ │ +│ │ 0x7F3A: │ │ +│ │ { │ │ +│ │ name: "Alice", │ │ +│ │ age: 25, │ │ +│ │ hobbies: ["reading", "gaming"] │ │ +│ │ } │ │ +│ └─────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────┐ │ +│ │ 0x8B2C: │ │ +│ │ [1, 2, 3, 4, 5] │ │ +│ └─────────────────────────────────────┘ │ +│ │ +│ Dynamic size, slower access │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### Putting It All Together + +When you create variables, here's what happens: + +```javascript +let name = "Alice"; // String stored on stack +let age = 25; // Number stored on stack +let user = { name: "Alice" }; // Reference on stack, object on heap +let scores = [95, 87, 92]; // Reference on stack, array on heap +``` + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ STACK AND HEAP TOGETHER │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ STACK HEAP │ +│ ┌─────────────────────┐ ┌────────────────────────────┐ │ +│ │ name = "Alice" │ │ │ │ +│ ├─────────────────────┤ │ ┌──────────────────────┐ │ │ +│ │ age = 25 │ │ │ { name: "Alice" } │ │ │ +│ ├─────────────────────┤ │ └──────────────────────┘ │ │ +│ │ user = 0x001 ─────┼───────────┼──────────▲ │ │ +│ ├─────────────────────┤ │ │ │ +│ │ scores = 0x002 ─────┼───────────┼───┐ ┌──────────────────┐ │ │ +│ └─────────────────────┘ │ └─►│ [95, 87, 92] │ │ │ +│ │ └──────────────────┘ │ │ +│ └────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Copying Behavior: The Critical Difference + +This is where things get interesting, and where bugs love to hide. + +### Copying Primitives: Independent Copies + +When you copy a primitive, you get a completely independent value: + +```javascript +let a = 10; +let b = a; // b gets a COPY of the value 10 + +b = 20; // changing b has NO effect on a + +console.log(a); // 10 (unchanged!) +console.log(b); // 20 +``` + +**What happens in memory:** + +``` +STEP 1: let a = 10 STEP 2: let b = a STEP 3: b = 20 + +┌──────────────┐ ┌──────────────┐ ┌──────────────┐ +│ a = 10 │ │ a = 10 │ │ a = 10 │ +└──────────────┘ ├──────────────┤ ├──────────────┤ + │ b = 10 │ (copy!) │ b = 20 │ + └──────────────┘ └──────────────┘ + + Two independent Changing b + copies of 10 doesn't touch a +``` + +### Copying Objects: Shared References + +When you copy an object, you copy the *reference*. Both variables now point to the SAME object: + +```javascript +let obj1 = { name: "Alice" }; +let obj2 = obj1; // obj2 gets a COPY of the REFERENCE + +obj2.name = "Bob"; // modifies the SAME object! + +console.log(obj1.name); // "Bob" (changed!) +console.log(obj2.name); // "Bob" +``` + +**What happens in memory:** + +``` +STEP 1: Create obj1 STEP 2: let obj2 = obj1 + +STACK HEAP STACK HEAP +┌────────────┐ ┌──────────────────┐ ┌────────────┐ ┌──────────────────┐ +│obj1 = 0x01─┼─►│ { name: "Alice" }│ │obj1 = 0x01─┼──►│ { name: "Alice" }│ +└────────────┘ └──────────────────┘ ├────────────┤ │ │ + │obj2 = 0x01─┼──► (same object!) │ + └────────────┘ └──────────────────┘ + +STEP 3: obj2.name = "Bob" + +STACK HEAP +┌────────────┐ ┌──────────────────┐ +│obj1 = 0x01─┼──►│ { name: "Bob" } │ ← Both see this change! +├────────────┤ │ │ +│obj2 = 0x01─┼──► (same object!) │ +└────────────┘ └──────────────────┘ +``` + +### The Array Surprise + +Arrays are objects too, so they behave the same way: + +```javascript +let arr1 = [1, 2, 3]; +let arr2 = arr1; // arr2 points to the SAME array + +arr2.push(4); // modifies the shared array + +console.log(arr1); // [1, 2, 3, 4] — Wait, what?! +console.log(arr2); // [1, 2, 3, 4] +``` + +<Warning> +**This catches EVERYONE at first!** When you write `let arr2 = arr1`, you're NOT creating a new array. You're creating a second variable that points to the same array. Any changes through either variable affect both. +</Warning> + +--- + +## Comparison Behavior + +### Primitives: Compared by Value + +Two primitives are equal if they have the same value: + +```javascript +let a = "hello"; +let b = "hello"; +console.log(a === b); // true — same value + +let x = 42; +let y = 42; +console.log(x === y); // true — same value +``` + +### Objects: Compared by Reference + +Two objects are equal only if they are the SAME object (same reference): + +```javascript +let obj1 = { name: "Alice" }; +let obj2 = { name: "Alice" }; +console.log(obj1 === obj2); // false — different objects! + +let obj3 = obj1; +console.log(obj1 === obj3); // true — same reference +``` + +**Visual explanation:** + +``` +obj1 and obj2: Different objects, same contents + +STACK HEAP +┌────────────┐ ┌──────────────────┐ +│obj1 = 0x01─┼─►│ { name: "Alice" }│ ← Object A +├────────────┤ └──────────────────┘ +│obj2 = 0x02─┼─►┌──────────────────┐ +└────────────┘ │ { name: "Alice" }│ ← Object B (different!) + └──────────────────┘ + +obj1 === obj2? 0x01 === 0x02? FALSE! +``` + +``` +obj1 and obj3: Same object + +STACK HEAP +┌────────────┐ ┌──────────────────┐ +│obj1 = 0x01─┼──►│ { name: "Alice" }│ +├────────────┤ │ │ +│obj3 = 0x01─┼──► (same object!) │ +└────────────┘ └──────────────────┘ + +obj1 === obj3? 0x01 === 0x01? TRUE! +``` + +### The Empty Object/Array Trap + +```javascript +console.log({} === {}); // false — two different empty objects +console.log([] === []); // false — two different empty arrays +console.log([1,2] === [1,2]); // false — two different arrays +``` + +<Tip> +**How to compare objects/arrays by content:** + +```javascript +// Simple (but limited) approach +JSON.stringify(obj1) === JSON.stringify(obj2) + +// For arrays of primitives +arr1.length === arr2.length && arr1.every((v, i) => v === arr2[i]) + +// For complex cases, use a library like Lodash +_.isEqual(obj1, obj2) +``` + +**Caution with JSON.stringify:** Property order matters! `{a:1, b:2}` and `{b:2, a:1}` will produce different strings even though they're logically equal. It also fails with `undefined`, functions, Symbols, circular references, `NaN`, and `Infinity`. For reliable deep equality, use a library like Lodash's `_.isEqual()`. +</Tip> + +--- + +## Functions and Parameters + +Here's a topic that confuses even experienced developers: + +**JavaScript is ALWAYS "pass by value"**, but when passing objects, the *value being passed is a reference*. + +### Passing Primitives + +When you pass a primitive to a function, the function receives a copy: + +```javascript +function double(num) { + num = num * 2; // changes the LOCAL copy only + return num; +} + +let x = 10; +let result = double(x); + +console.log(x); // 10 (unchanged!) +console.log(result); // 20 +``` + +**What happens:** + +``` +BEFORE double(x): INSIDE double(x): AFTER double(x): + +┌──────────────┐ ┌──────────────┐ ┌──────────────┐ +│ x = 10 │ │ x = 10 │ │ x = 10 │ +└──────────────┘ ├──────────────┤ └──────────────┘ + │ num = 10 │ (copy) + └──────────────┘ + ↓ + ┌──────────────┐ + │ num = 20 │ (modified copy) + └──────────────┘ +``` + +### Passing Objects: Mutation WORKS + +When you pass an object, the function receives a copy of the reference. Both point to the same object: + +```javascript +function rename(person) { + person.name = "Bob"; // mutates the ORIGINAL object! +} + +let user = { name: "Alice" }; +rename(user); + +console.log(user.name); // "Bob" — changed! +``` + +**What happens:** + +``` +BEFORE rename(user): INSIDE rename(user): + +STACK HEAP STACK HEAP +┌────────────┐ ┌────────────┐ +│user = 0x01─┼─►{ name: │user = 0x01─┼──►{ name: "Bob" } +└────────────┘ "Alice" } ├────────────┤ ▲ + │person=0x01─┼───────┘ + └────────────┘ (same object!) +``` + +### Passing Objects: Reassignment DOESN'T Work + +But if you reassign the parameter, you're only changing the local copy of the reference: + +```javascript +function replace(person) { + person = { name: "Charlie" }; // creates NEW local object +} + +let user = { name: "Alice" }; +replace(user); + +console.log(user.name); // "Alice" — unchanged! +``` + +**What happens:** + +``` +INSIDE replace(user): + +STACK HEAP +┌────────────┐ ┌─────────────────┐ +│user = 0x01─┼─►│ { name: "Alice" }│ ← Original, unchanged +├────────────┤ └─────────────────┘ +│person=0x02─┼─►┌──────────────────┐ +└────────────┘ │ { name: "Charlie" }│ ← New object, discarded + └──────────────────┘ +``` + +<Info> +**The key insight:** You can *mutate* an object through a function parameter, but you cannot *replace* the original object. Reassigning the parameter only changes where that local variable points. +</Info> + +--- + +## Mutation vs Reassignment + +Understanding this distinction is crucial for avoiding bugs. + +### Mutation: Changing the Contents + +Mutation modifies the existing object in place: + +```javascript +const arr = [1, 2, 3]; + +// These are all MUTATIONS: +arr.push(4); // [1, 2, 3, 4] +arr[0] = 99; // [99, 2, 3, 4] +arr.pop(); // [99, 2, 3] +arr.sort(); // modifies in place + +const obj = { name: "Alice" }; + +// These are all MUTATIONS: +obj.name = "Bob"; // changes property +obj.age = 25; // adds property +delete obj.age; // removes property +``` + +### Reassignment: Pointing to a New Value + +Reassignment makes the variable point to something else entirely: + +```javascript +let arr = [1, 2, 3]; +arr = [4, 5, 6]; // REASSIGNMENT — new array + +let obj = { name: "Alice" }; +obj = { name: "Bob" }; // REASSIGNMENT — new object +``` + +### The `const` Trap + +`const` prevents **reassignment** but NOT **mutation**: + +```javascript +const arr = [1, 2, 3]; + +// ✅ Mutations are ALLOWED: +arr.push(4); // works! +arr[0] = 99; // works! + +// ❌ Reassignment is BLOCKED: +arr = [4, 5, 6]; // TypeError: Assignment to constant variable + +const obj = { name: "Alice" }; + +// ✅ Mutations are ALLOWED: +obj.name = "Bob"; // works! +obj.age = 25; // works! + +// ❌ Reassignment is BLOCKED: +obj = { name: "Eve" }; // TypeError: Assignment to constant variable +``` + +<Warning> +**Common misconception:** Many developers think `const` creates an "immutable" variable. It doesn't! It only prevents reassignment. The contents of objects and arrays declared with `const` can still be changed. +</Warning> + +--- + +## True Immutability with [`Object.freeze()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze) + +We learned that `const` doesn't make objects immutable. It only prevents reassignment. But what if you NEED a truly immutable object? + +### Object.freeze(): Shallow Immutability + +`Object.freeze()` prevents any changes to an object's properties: + +```javascript +const user = Object.freeze({ name: "Alice", age: 25 }); + +user.name = "Bob"; // Silently fails (or throws in strict mode) +user.email = "a@b.com"; // Can't add properties +delete user.age; // Can't delete properties + +console.log(user); // { name: "Alice", age: 25 } — unchanged! +``` + +**What Object.freeze() prevents:** +- Changing existing property values +- Adding new properties +- Deleting properties +- Changing property descriptors (like making a property writable) + +### Checking If an Object Is Frozen + +```javascript +const frozen = Object.freeze({ a: 1 }); +const normal = { a: 1 }; + +console.log(Object.isFrozen(frozen)); // true +console.log(Object.isFrozen(normal)); // false +``` + +<Warning> +**Object.freeze() is shallow!** It only freezes the top level. Nested objects can still be modified: + +```javascript +const user = Object.freeze({ + name: "Alice", + address: { city: "NYC" } +}); + +user.name = "Bob"; // Blocked +user.address.city = "LA"; // Works! Nested object not frozen + +console.log(user.address.city); // "LA" +``` +</Warning> + +### Deep Freeze: Making Everything Immutable + +To freeze nested objects too, you need a recursive "deep freeze" function: + +```javascript +function deepFreeze(obj, seen = new WeakSet()) { + // Prevent infinite loops from circular references + if (seen.has(obj)) return obj; + seen.add(obj); + + // Get all property names (including symbols) + const propNames = Reflect.ownKeys(obj); + + // Freeze nested objects first + for (const name of propNames) { + const value = obj[name]; + if (value && typeof value === "object") { + deepFreeze(value, seen); + } + } + + // Then freeze the object itself + return Object.freeze(obj); +} + +const user = deepFreeze({ + name: "Alice", + address: { city: "NYC" } +}); + +user.address.city = "LA"; // Now this is blocked too! +console.log(user.address.city); // "NYC" +``` + +<Info> +**Why the `seen` WeakSet?** Objects can have circular references (e.g., `obj.self = obj`). Without tracking visited objects, the function would recurse infinitely. The WeakSet ensures each object is only processed once. +</Info> + +### Related Methods: freeze vs seal vs preventExtensions + +| Method | Add Properties | Delete Properties | Change Values | +|--------|:-------------:|:-----------------:|:-------------:| +| `Object.freeze()` | No | No | No | +| `Object.seal()` | No | No | **Yes** | +| `Object.preventExtensions()` | No | **Yes** | **Yes** | + +```javascript +// Object.seal() — can change values, but can't add/delete +const sealed = Object.seal({ name: "Alice" }); +sealed.name = "Bob"; // Works! +sealed.age = 25; // Fails — can't add +delete sealed.name; // Fails — can't delete + +// Object.preventExtensions() — can change/delete, but can't add +const noExtend = Object.preventExtensions({ name: "Alice" }); +noExtend.name = "Bob"; // Works! +delete noExtend.name; // Works! +noExtend.age = 25; // Fails — can't add +``` + +<Tip> +**When to use Object.freeze():** +- Configuration objects that should never change +- Constants or enums in your application +- Protecting objects passed to untrusted code +- Debugging mutation bugs (freeze the object and see what breaks!) +</Tip> + +--- + +## Shallow Copy vs Deep Copy + +When you need a truly independent copy of an object, you have two options. + +### Shallow Copy: One Level Deep + +A shallow copy creates a new object with copies of the top-level properties. But nested objects are still shared! + +**Shallow copy methods:** + +```javascript +const original = { + name: "Alice", + scores: [95, 87, 92], + address: { city: "NYC" } +}; + +// Method 1: Spread operator +const copy1 = { ...original }; + +// Method 2: Object.assign +const copy2 = Object.assign({}, original); + +// For arrays: +const arrCopy1 = [...originalArray]; +const arrCopy2 = originalArray.slice(); +const arrCopy3 = Array.from(originalArray); +``` + +**The problem with shallow copy:** + +```javascript +const original = { + name: "Alice", + address: { city: "NYC" } +}; + +const shallow = { ...original }; + +// Top-level changes are independent: +shallow.name = "Bob"; +console.log(original.name); // "Alice" ✅ + +// But nested objects are SHARED: +shallow.address.city = "LA"; +console.log(original.address.city); // "LA" 😱 +``` + +**Visual explanation:** + +``` +SHALLOW COPY + +original shallow +┌─────────────────┐ ┌─────────────────┐ +│ name: "Alice" │ │ name: "Alice" │ (independent copy) +│ address: 0x01 ──┼────┐ │ address: 0x01 ──┼────┐ +└─────────────────┘ │ └─────────────────┘ │ + │ │ + ▼ ▼ + ┌─────────────────┐ + │ { city: "NYC" } │ ← SHARED! Both point here + └─────────────────┘ +``` + +### Deep Copy: All Levels + +A deep copy creates completely independent copies at every level. + +**Method 1: [`structuredClone()`](https://developer.mozilla.org/en-US/docs/Web/API/structuredClone) (Recommended)** + +```javascript +const original = { + name: "Alice", + scores: [95, 87, 92], + address: { city: "NYC" }, + date: new Date() +}; + +const deep = structuredClone(original); + +// Now everything is independent: +deep.address.city = "LA"; +console.log(original.address.city); // "NYC" ✅ + +deep.scores.push(100); +console.log(original.scores); // [95, 87, 92] ✅ +``` + +**Method 2: JSON trick (has limitations)** + +```javascript +const deep = JSON.parse(JSON.stringify(original)); +``` + +### Comparing the Methods + +<Tabs> + <Tab title="structuredClone()"> + **Pros:** + - Handles most types correctly + - Preserves Dates, Maps, Sets, ArrayBuffers + - Handles circular references + - Built into JavaScript (ES2022+) + + **Cons:** + - Cannot clone functions + - Cannot clone DOM nodes + - Cannot clone property descriptors, getters/setters + - Cannot clone the prototype chain + - Symbol-keyed properties are ignored + - Error objects lose their stack trace (only `message` is preserved) + - Not available in older browsers (pre-2022) + + ```javascript + const obj = { + date: new Date(), + set: new Set([1, 2, 3]), + map: new Map([["key", "value"]]) + }; + + const clone = structuredClone(obj); + // Works perfectly! ✅ + ``` + </Tab> + <Tab title="JSON.parse/stringify"> + **Pros:** + - Works in all browsers + - Simple to use + + **Cons:** + - **Loses functions** — they disappear + - **Loses undefined** — properties with undefined are removed + - **Converts Dates to strings** — not Date objects + - **Cannot handle circular references** — throws error + - **Loses Symbol keys** — they're ignored + - **Loses Map/Set** — converted to empty objects + + ```javascript + const obj = { + fn: () => {}, // ❌ lost + date: new Date(), // ❌ becomes string + undef: undefined, // ❌ property removed + set: new Set([1, 2]) // ❌ becomes {} + }; + + const clone = JSON.parse(JSON.stringify(obj)); + // { date: "2025-12-29T..." } — Most data lost! + ``` + </Tab> +</Tabs> + +<Tip> +**Which to use:** +- **`structuredClone()`** — Use this for most cases (modern browsers) +- **JSON trick** — Only for simple objects with no functions, dates, or special types +- **Lodash `_.cloneDeep()`** — When you need maximum compatibility +</Tip> + +--- + +## Common Bugs and Pitfalls + +<AccordionGroup> + <Accordion title="1. Accidental Object/Array Mutation"> + ```javascript + // BUG: Modifying function parameter + function processUsers(users) { + users.push({ name: "New User" }); // Mutates original! + return users; + } + + const myUsers = [{ name: "Alice" }]; + processUsers(myUsers); + console.log(myUsers); // [{ name: "Alice" }, { name: "New User" }] + + // FIX: Create a copy first + function processUsers(users) { + const copy = [...users]; + copy.push({ name: "New User" }); + return copy; + } + ``` + </Accordion> + + <Accordion title="2. Array Methods That Mutate"> + ```javascript + // These MUTATE the original array: + arr.push() arr.pop() + arr.shift() arr.unshift() + arr.splice() arr.sort() + arr.reverse() arr.fill() + + // These RETURN a new array (safe): + arr.map() arr.filter() + arr.slice() arr.concat() + arr.flat() arr.flatMap() + arr.toSorted() arr.toReversed() // ES2023 + arr.toSpliced() // ES2023 + + // GOTCHA: sort() mutates! + const nums = [3, 1, 2]; + const sorted = nums.sort(); // nums is NOW [1, 2, 3]! + + // FIX: Copy first, or use toSorted() + const sorted = [...nums].sort(); + const sorted = nums.toSorted(); // ES2023 + ``` + </Accordion> + + <Accordion title="3. Comparing Objects/Arrays"> + ```javascript + // BUG: This will NEVER work + if (user1 === user2) { } // Compares references + if (arr1 === arr2) { } // Compares references + if (config === defaultConfig) { } // Compares references + + // Even these fail: + [] === [] // false + {} === {} // false + [1, 2] === [1, 2] // false + + // FIX: Compare contents + JSON.stringify(a) === JSON.stringify(b) // Simple but limited + + // Or use a deep equality function + function deepEqual(a, b) { + return JSON.stringify(a) === JSON.stringify(b); + } + ``` + </Accordion> + + <Accordion title="4. Shallow Copy with Nested Objects"> + ```javascript + // BUG: Shallow copy doesn't clone nested objects + const user = { + name: "Alice", + settings: { theme: "dark" } + }; + + const copy = { ...user }; + copy.settings.theme = "light"; + + console.log(user.settings.theme); // "light" — Original changed! + + // FIX: Use deep copy + const copy = structuredClone(user); + ``` + </Accordion> + + <Accordion title="5. Shared Default Object Reference"> + ```javascript + // BUG: Default object gets mutated across calls + function addItem(item, list = []) { + list.push(item); + return list; + } + + // This works fine with primitives, but with objects... + // Actually, this specific case is fine because default + // parameters are evaluated fresh each call. + + // But THIS is a problem: + const defaultList = []; + function addItem(item, list = defaultList) { + list.push(item); + return list; + } + + addItem("a"); // ["a"] + addItem("b"); // ["a", "b"] — defaultList was mutated! + ``` + </Accordion> + + <Accordion title="6. Forgetting Arrays Are Reference Types"> + ```javascript + // BUG: Thinking you have two arrays + const original = [1, 2, 3]; + const backup = original; // NOT a backup! + + original.push(4); + console.log(backup); // [1, 2, 3, 4] — "backup" changed! + + // FIX: Actually copy the array + const backup = [...original]; + const backup = original.slice(); + ``` + </Accordion> + + <Accordion title="7. Memory Leaks from Forgotten References (Advanced)"> + When you store objects in Maps or arrays, those references prevent garbage collection, even if you don't need the object anymore. + + ```javascript + // Potential memory leak: cache holds references forever + const cache = new Map(); + + function processUser(user) { + cache.set(user.id, user); // user can never be garbage collected + } + + // Even if user objects are no longer needed elsewhere, + // the cache keeps them alive in memory + ``` + + **Solutions:** + + - **[WeakMap](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap)** — Keys are "weakly held" and can be garbage collected + - **Manual cleanup** — Delete entries when done + + ```javascript + // WeakMap: objects can be garbage collected when no other references exist + const cache = new WeakMap(); + + function processUser(user) { + cache.set(user, computeExpensiveData(user)); + } + // When 'user' object is no longer referenced elsewhere, + // it AND its cached data can be garbage collected + ``` + + <Info> + **WeakMap vs Map:** WeakMap keys must be objects (not primitives), and you can't iterate over a WeakMap. Use it when you want cached data to automatically disappear when the source object is gone. + </Info> + </Accordion> +</AccordionGroup> + +--- + +## Best Practices + +<Tip> +**Guidelines for working with reference types:** + +1. **Treat objects as immutable when possible** + ```javascript + // Instead of mutating: + user.name = "Bob"; + + // Create a new object: + const updatedUser = { ...user, name: "Bob" }; + ``` + +2. **Use `const` by default** — prevents accidental reassignment + +3. **Know which methods mutate** + - Mutating: `push`, `pop`, `sort`, `reverse`, `splice` + - Non-mutating: `map`, `filter`, `slice`, `concat`, `toSorted` + +4. **Use `structuredClone()` for deep copies** + ```javascript + const clone = structuredClone(original); + ``` + +5. **Be explicit about intent** — comment when mutating on purpose + ```javascript + // Intentionally mutating for performance + largeArray.sort((a, b) => a - b); + ``` + +6. **Clone function parameters if you need to modify them** + ```javascript + function processData(data) { + const copy = structuredClone(data); + // Now safe to modify copy + } + ``` +</Tip> + +--- + +## Key Takeaways + +<Info> +**The key things to remember:** + +1. **Value types** (primitives) store values directly; **reference types** store pointers + +2. **Copying a primitive** creates an independent copy — changing one doesn't affect the other + +3. **Copying an object/array** copies the reference — both point to the SAME data + +4. **Comparison:** primitives compare by value, objects compare by reference + +5. **Functions:** JavaScript passes everything by value, but object values ARE references + +6. **Mutation ≠ Reassignment:** `const` only prevents reassignment, not mutation + +7. **Shallow copy** (spread, Object.assign) only copies one level — nested objects are shared + +8. **Deep copy** with `structuredClone()` creates completely independent copies + +9. **Know your array methods:** `push/pop/sort` mutate; `map/filter/slice` don't + +10. **True immutability** requires `Object.freeze()` — but it's shallow, so use deep freeze for nested objects +</Info> + +--- + +## Test Your Knowledge + +<AccordionGroup> + <Accordion title="Question 1: What's the difference between value types and reference types?"> + **Answer:** + + - **Value types (primitives)** store the actual value directly. Copying creates an independent copy, and comparison checks if values are the same. + + - **Reference types (objects, arrays, functions)** store a pointer/reference to the data. Copying creates another pointer to the SAME data, and comparison checks if both point to the same object. + </Accordion> + + <Accordion title="Question 2: What does this code output?"> + ```javascript + let a = { count: 1 }; + let b = a; + b.count = 5; + console.log(a.count); + ``` + + **Answer:** `5` + + Both `a` and `b` point to the same object. When you modify `b.count`, you're modifying the shared object, which `a` also sees. + </Accordion> + + <Accordion title="Question 3: Why does {} === {} return false?"> + **Answer:** Because `===` compares references, not contents. + + Each `{}` creates a NEW empty object in memory with a different reference. Even though they have the same contents (both empty), they are different objects at different memory addresses. + + ```javascript + {} === {} // false (different objects) + + const a = {}; + const b = a; + a === b // true (same reference) + ``` + </Accordion> + + <Accordion title="Question 4: What's the difference between shallow and deep copy?"> + **Answer:** + + - **Shallow copy** creates a new object and copies top-level properties. Nested objects are NOT copied — they're still shared references. + + - **Deep copy** creates a completely independent copy at ALL levels. Nested objects are also cloned. + + ```javascript + const original = { nested: { value: 1 } }; + + // Shallow: nested is shared + const shallow = { ...original }; + shallow.nested.value = 2; + console.log(original.nested.value); // 2 (affected!) + + // Deep: completely independent + const deep = structuredClone(original); + deep.nested.value = 3; + console.log(original.nested.value); // 2 (unchanged) + ``` + </Accordion> + + <Accordion title="Question 5: Does const prevent object mutation?"> + **Answer:** No! + + `const` only prevents **reassignment** — you can't make the variable point to a different value. But you CAN still **mutate** the object's contents. + + ```javascript + const obj = { name: "Alice" }; + + obj.name = "Bob"; // ✅ Allowed (mutation) + obj.age = 25; // ✅ Allowed (mutation) + obj = {}; // ❌ Error (reassignment) + ``` + </Accordion> + + <Accordion title="Question 6: What happens when you pass an object to a function and modify it?"> + **Answer:** The original object IS modified! + + When you pass an object to a function, the function receives a copy of the reference. Both the original variable and the function parameter point to the same object. Mutations through either will affect the shared object. + + ```javascript + function addAge(person) { + person.age = 30; // Mutates the original! + } + + const user = { name: "Alice" }; + addAge(user); + console.log(user.age); // 30 + ``` + + However, if you *reassign* the parameter inside the function, it only changes the local variable, not the original. + </Accordion> + + <Accordion title="Question 7: Does Object.freeze() make nested objects immutable?"> + **Answer:** No! `Object.freeze()` is shallow. + + It only freezes the top-level properties. Nested objects can still be modified: + + ```javascript + const user = Object.freeze({ + name: "Alice", + address: { city: "NYC" } + }); + + user.name = "Bob"; // Blocked (frozen) + user.address.city = "LA"; // Works! (nested object not frozen) + + console.log(user.address.city); // "LA" + ``` + + To freeze everything, you need a recursive "deep freeze" function that freezes all nested objects. + </Accordion> + + <Accordion title="Question 8: What's the difference between Object.freeze() and const?"> + **Answer:** They protect different things: + + - **`const`** prevents **reassignment** — you can't make the variable point to a different value + - **`Object.freeze()`** prevents **mutation** — you can't change the object's properties + + ```javascript + // const alone: can mutate, can't reassign + const obj1 = { a: 1 }; + obj1.a = 2; // ✅ Works + obj1 = {}; // ❌ Error + + // freeze alone: can reassign (if let), can't mutate + let obj2 = Object.freeze({ a: 1 }); + obj2.a = 2; // ❌ Fails silently + obj2 = {}; // ✅ Works (it's let) + + // Both together: can't do either + const obj3 = Object.freeze({ a: 1 }); + obj3.a = 2; // ❌ Fails silently + obj3 = {}; // ❌ Error + ``` + </Accordion> +</AccordionGroup> + +--- + +## Related Concepts + +<CardGroup cols={2}> + <Card title="Primitive Types" icon="atom" href="/concepts/primitive-types"> + Deep dive into the 7 primitive types and their characteristics + </Card> + <Card title="Type Coercion" icon="shuffle" href="/concepts/type-coercion"> + How JavaScript converts between types automatically + </Card> + <Card title="Scope and Closures" icon="layer-group" href="/concepts/scope-and-closures"> + How closures capture references to variables + </Card> + <Card title="Equality Operators" icon="equals" href="/concepts/equality-operators"> + Understanding == vs === and type checking + </Card> +</CardGroup> + +--- + +## Reference + +<CardGroup cols={2}> + <Card title="JavaScript Data Types — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures"> + Official documentation on JavaScript's type system, including primitives and objects. + </Card> + <Card title="Object.freeze() — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze"> + Documentation on freezing objects for immutability. + </Card> + <Card title="structuredClone() — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/structuredClone"> + The modern way to create deep copies of objects. + </Card> + <Card title="WeakMap — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap"> + Maps with weak references that allow garbage collection. + </Card> +</CardGroup> + +## Articles + +<CardGroup cols={2}> + <Card title="Explaining Value vs. Reference in Javascript" icon="newspaper" href="https://codeburst.io/explaining-value-vs-reference-in-javascript-647a975e12a0"> + Clear explanation by Arnav Aggarwal with visual diagrams showing how primitives and objects behave differently in memory. + </Card> + <Card title="JavaScript Primitive vs. Reference Values" icon="newspaper" href="https://www.javascripttutorial.net/javascript-primitive-vs-reference-values/"> + JavaScript Tutorial's guide includes animated diagrams showing stack vs heap memory allocation. Covers copying behavior and function parameter passing with runnable code examples. + </Card> + <Card title="Back to roots: JavaScript Value vs Reference" icon="newspaper" href="https://medium.com/dailyjs/back-to-roots-javascript-value-vs-reference-8fb69d587a18"> + Miro Koczka explains why `const` doesn't make objects immutable and how to avoid accidental mutations. Includes common bug patterns and fixes. + </Card> + <Card title="You Don't Know JS: Types & Grammar" icon="book" href="https://github.com/getify/You-Dont-Know-JS/blob/2nd-ed/types-grammar/README.md"> + Kyle Simpson's definitive guide to JavaScript's type system. Free to read online. + </Card> +</CardGroup> + +## Videos + +<CardGroup cols={2}> + <Card title="JavaScript Value vs Reference Types — Programming with Mosh" icon="video" href="https://www.youtube.com/watch?v=fD0t_DKREbE"> + Mosh uses whiteboard diagrams to show exactly how primitives live on the stack while objects live on the heap. 10-minute video that makes memory concepts click. + </Card> + <Card title="Reference vs Primitive Values — Academind" icon="video" href="https://www.youtube.com/watch?v=9ooYYRLdg_g"> + Academind's Max demonstrates mutation bugs in real-time, then shows how to fix them with spread operators and structuredClone. Good for seeing the problems before learning solutions. + </Card> + <Card title="Javascript Pass by Value vs Pass by Reference — techsith" icon="video" href="https://www.youtube.com/watch?v=E-dAnFdq8k8"> + Focused explanation of how function parameters work with primitives vs objects. Great for understanding the "pass by value of the reference" concept. + </Card> +</CardGroup> diff --git a/docs/concepts/web-workers.mdx b/docs/concepts/web-workers.mdx new file mode 100644 index 00000000..2d753804 --- /dev/null +++ b/docs/concepts/web-workers.mdx @@ -0,0 +1,1653 @@ +--- +title: "Web Workers: True Parallelism in JavaScript" +sidebarTitle: "Web Workers: True Parallelism" +description: "Learn Web Workers in JavaScript for running code in background threads. Understand postMessage, Dedicated and Shared Workers, and transferable objects." +--- + +Ever clicked a button and watched your entire page freeze? Tried to scroll while a script was running and nothing happened? + +```javascript +// This will freeze your entire page for ~5 seconds +function heavyCalculation() { + const start = Date.now() + while (Date.now() - start < 5000) { + // Simulating heavy work + } + return 'Done!' +} + +document.getElementById('btn').addEventListener('click', () => { + console.log('Starting...') + const result = heavyCalculation() // Page freezes here + console.log(result) +}) + +// During those 5 seconds: +// - Can't click anything +// - Can't scroll +// - Animations stop +// - The page looks broken +``` + +That's JavaScript's single thread at work. But there's a way out: **[Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API)**. They let you run JavaScript in background threads, keeping your UI smooth while crunching numbers, parsing data, or processing images. + +<Info> +**What you'll learn in this guide:** +- Why JavaScript's single thread causes UI freezes (and why async doesn't help) +- How Web Workers provide true parallelism (not just concurrency) +- Creating workers and communicating with `postMessage` +- The difference between Dedicated, Shared, and Service Workers +- Transferable objects for moving large data without copying +- OffscreenCanvas for graphics processing in workers +- Real-world patterns: worker pools, inline workers, heavy computations +</Info> + +<Warning> +**Prerequisites:** This guide builds on [the Event Loop](/concepts/event-loop) and [async/await](/concepts/async-await). Understanding those concepts will help you see why Web Workers solve problems that async code can't. +</Warning> + +--- + +## The Problem: Why Async Isn't Enough + +You might think: "I already know async JavaScript. Doesn't that solve the freezing problem?" + +Not quite. Here's the thing everyone gets wrong about async: **async JavaScript is still single-threaded**. It's concurrent, not parallel. + +```javascript +// Async code is NOT running at the same time +async function fetchData() { + console.log('1: Starting fetch') + const response = await fetch('/api/data') // Waits, but doesn't block + console.log('3: Got response') + return response.json() +} + +console.log('0: Before fetch') +fetchData() +console.log('2: After fetch call') + +// Output: +// 0: Before fetch +// 1: Starting fetch +// 2: After fetch call +// 3: Got response (later) +``` + +The `await` lets other code run while waiting for the network. But here's the catch: **the actual JavaScript execution is still one thing at a time**. + +### The CPU-Bound Problem + +Async works great for I/O operations (network requests, file reads) because you're waiting for something external. But what about CPU-bound tasks? + +```javascript +// This async function STILL freezes the page +async function processLargeArray(data) { + const results = [] + + // This loop is synchronous JavaScript + // The "async" keyword doesn't help here! + for (let i = 0; i < data.length; i++) { + results.push(expensiveCalculation(data[i])) + } + + return results +} + +// The page freezes during the loop +// async/await only helps with WAITING, not COMPUTING +``` + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ ASYNC VS PARALLEL: THE DIFFERENCE │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ASYNC (Concurrency) PARALLEL (Web Workers) │ +│ ──────────────────── ───────────────────── │ +│ │ +│ Main Thread Main Thread Worker Thread │ +│ ┌─────────────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ Task A │ │ Task A │ │ Task B │ │ +│ │ (work) │ │ (work) │ │ (work) │ │ +│ ├─────────────────┤ │ │ │ │ │ +│ │ Wait for I/O... │ ← yields │ │ │ │ │ +│ ├─────────────────┤ │ │ │ │ │ +│ │ Task B │ │ │ │ │ │ +│ │ (work) │ │ │ │ │ │ +│ ├─────────────────┤ └──────────┘ └──────────┘ │ +│ │ Task A resumed │ │ +│ └─────────────────┘ Both run at the SAME TIME │ +│ on different CPU cores │ +│ One thread, tasks take turns │ +│ │ +│ GOOD FOR: Network requests, GOOD FOR: Heavy calculations, │ +│ file reads, timers image processing, data parsing │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +<Tip> +**The Rule:** Use async/await when you're **waiting** for something. Use Web Workers when you're **computing** something heavy. +</Tip> + +--- + +## The Restaurant Analogy: Multiple Chefs + +If you've read our [Event Loop guide](/concepts/event-loop), you know JavaScript is like a restaurant with a **single chef**. The chef can only cook one dish at a time, but clever scheduling (the event loop) keeps things moving. + +Web Workers are like **hiring more chefs**. + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ THE MULTI-CHEF KITCHEN (WEB WORKERS) │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ MAIN KITCHEN (Main Thread) PREP KITCHEN (Worker Thread) │ +│ ┌─────────────────────────┐ ┌─────────────────────────┐ │ +│ │ │ │ │ │ +│ │ HEAD CHEF │ │ PREP CHEF │ │ +│ │ ┌─────────┐ │ │ ┌─────────┐ │ │ +│ │ │ ^_^ │ │ │ │ ^_^ │ │ │ +│ │ └─────────┘ │ │ └─────────┘ │ │ +│ │ │ │ │ │ +│ │ • Takes customer │ │ • Chops vegetables │ │ +│ │ orders (events) │ │ • Preps ingredients │ │ +│ │ • Plates dishes (UI) │ │ • Heavy work │ │ +│ │ • Talks to customers │ │ • No customer contact │ │ +│ │ (DOM access) │ │ (no DOM!) │ │ +│ │ │ │ │ │ +│ └───────────┬─────────────┘ └───────────┬─────────────┘ │ +│ │ │ │ +│ │ ┌──────────────────┐ │ │ +│ │ │ SERVICE WINDOW │ │ │ +│ └─────►│ (postMessage) │◄─────────┘ │ +│ │ │ │ +│ │ "Need 50 onions │ │ +│ │ chopped!" │ │ +│ │ │ │ +│ │ "Here they are!"│ │ +│ └──────────────────┘ │ +│ │ +│ KEY RULES: │ +│ • Chefs can't share cutting boards (no shared memory by default) │ +│ • They communicate through the service window (postMessage) │ +│ • Prep chef can't talk to customers (workers can't touch the DOM) │ +│ • Prep chef has their own tools (workers have their own global scope) │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +| Kitchen | JavaScript | +|---------|------------| +| **Head Chef** | Main thread (handles UI, events, DOM) | +| **Prep Chef** | Web Worker (handles heavy computation) | +| **Service Window** | `postMessage()` / `onmessage` (communication) | +| **Cutting Board** | Memory (each chef has their own) | +| **Customers** | Users interacting with the page | +| **Kitchen Rules** | Worker limitations (no DOM access) | + +The prep chef works independently in their own kitchen. They can't talk to customers (no DOM access), but they can do heavy prep work without slowing down the head chef. When they're done, they pass the result through the service window. + +--- + +## What is a Web Worker? + +A **[Web Worker](https://developer.mozilla.org/en-US/docs/Web/API/Worker)** is a JavaScript script that runs in a background thread, separate from the main thread. It has its own global scope, its own event loop, and executes truly in parallel with your main code. Workers communicate with the main thread through message passing using `postMessage()` and `onmessage`. This lets you run expensive computations without freezing the UI. + +Here's a basic example: + +```javascript +// main.js - runs on the main thread +const worker = new Worker('worker.js') + +// Send data to the worker +worker.postMessage({ numbers: [1, 2, 3, 4, 5] }) + +// Receive results from the worker +worker.onmessage = (event) => { + console.log('Result from worker:', event.data) +} +``` + +```javascript +// worker.js - runs in a separate thread +self.onmessage = (event) => { + const { numbers } = event.data + + // Do heavy computation (won't freeze the UI!) + const sum = numbers.reduce((a, b) => a + b, 0) + + // Send result back to main thread + self.postMessage({ sum }) +} +``` + +<Note> +Inside a worker, `self` refers to the worker's global scope (a [`DedicatedWorkerGlobalScope`](https://developer.mozilla.org/en-US/docs/Web/API/DedicatedWorkerGlobalScope)). You can also use `this` at the top level, but `self` is clearer. +</Note> + +### The Communication Model + +Workers and the main thread communicate through **messages**. They can't directly access each other's variables. This is intentional: it prevents the race conditions and bugs that plague traditional multi-threaded programming. + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ WORKER COMMUNICATION │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ MAIN THREAD WORKER THREAD │ +│ ┌───────────────────────┐ ┌───────────────────────┐ │ +│ │ │ postMessage │ │ │ +│ │ const worker = │ ─────────────► │ self.onmessage = │ │ +│ │ new Worker(...) │ │ (event) => {...} │ │ +│ │ │ │ │ │ +│ │ worker.postMessage() │ │ // Do heavy work │ │ +│ │ │ │ │ │ +│ │ worker.onmessage = │ ◄───────────── │ self.postMessage() │ │ +│ │ (event) => {...} │ postMessage │ │ │ +│ │ │ │ │ │ +│ └───────────────────────┘ └───────────────────────┘ │ +│ │ +│ DATA IS COPIED (by default), not shared │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## How Do You Create a Web Worker? + +There are two ways to create workers: the **classic way** (original syntax) and the **module way** (modern, recommended). + +### Classic Workers + +The original way to create workers uses `importScripts()` for loading dependencies: + +```javascript +// main.js +const worker = new Worker('worker.js') + +worker.postMessage('Hello from main!') + +worker.onmessage = (event) => { + console.log('Worker said:', event.data) +} + +worker.onerror = (error) => { + console.error('Worker error:', error.message) +} +``` + +```javascript +// worker.js (classic style) +importScripts('https://example.com/some-library.js') // Load dependencies + +self.onmessage = (event) => { + console.log('Main said:', event.data) + + // Do some work... + + self.postMessage('Hello from worker!') +} +``` + +### Module Workers (Recommended) + +Modern browsers support module workers with `import`/`export`. This is cleaner and matches how you write other JavaScript: + +```javascript +// main.js +const worker = new Worker('worker.js', { type: 'module' }) + +worker.postMessage({ task: 'process', data: [1, 2, 3] }) + +worker.onmessage = (event) => { + console.log('Result:', event.data) +} +``` + +```javascript +// worker.js (module style) +import { processData } from './utils.js' // Standard ES modules! + +self.onmessage = (event) => { + const { task, data } = event.data + + if (task === 'process') { + const result = processData(data) + self.postMessage(result) + } +} +``` + +<Tip> +**Use module workers** whenever possible. They support `import`/`export`, have strict mode by default, and work better with modern tooling. Check [browser support](https://caniuse.com/mdn-api_worker_worker_options_type_parameter) before using in production. +</Tip> + +### Comparison: Classic vs Module Workers + +| Feature | Classic Worker | Module Worker | +|---------|---------------|---------------| +| **Syntax** | `new Worker('file.js')` | `new Worker('file.js', { type: 'module' })` | +| **Dependencies** | `importScripts()` | `import` / `export` | +| **Strict mode** | Optional | Always on | +| **Top-level await** | No | Yes | +| **Browser support** | All browsers | Modern browsers | +| **Tooling** | Limited | Works with bundlers | + +--- + +## How Does postMessage Work? + +Communication between workers and the main thread happens through [`postMessage()`](https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage). Understanding how data is transferred is important for performance. + +### The Structured Clone Algorithm + +When you send data via `postMessage`, it's **copied** using the [structured clone algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm). This is deeper than `JSON.stringify`: it handles more types, preserves object references within the data, and even supports circular references. + +<Tip> +You can use the global [`structuredClone()`](https://developer.mozilla.org/en-US/docs/Web/API/structuredClone) function to deep-clone objects using the same algorithm. This is useful for copying complex data outside of worker communication: + +```javascript +const original = { + name: 'Alice', + date: new Date(), + nested: { deep: true } +} + +// Deep clone with structuredClone (handles Date, Map, Set, etc.) +const clone = structuredClone(original) + +clone.name = 'Bob' +console.log(original.name) // 'Alice' (unchanged) +console.log(clone.date instanceof Date) // true (Date preserved!) +``` +</Tip> + +```javascript +// main.js +const data = { + name: 'Alice', + scores: [95, 87, 92], + metadata: { + date: new Date(), + pattern: /test/gi + } +} + +worker.postMessage(data) +// The worker receives a COPY of this object +// Modifying it in the worker won't affect the original +``` + +### What Can Be Cloned? + +| Can Clone | Cannot Clone | +|-----------|--------------| +| Primitives (string, number, boolean, null, undefined) | Functions | +| Plain objects and arrays | DOM nodes | +| Date objects | Symbols | +| RegExp objects | WeakMap, WeakSet | +| Blob, File, FileList | Objects with prototype chains | +| ArrayBuffer, TypedArrays | Getters/setters | +| Map, Set | Proxies | +| Error objects (standard types) | | +| ImageBitmap, ImageData | | + +<Note> +**Error cloning:** Only standard error types can be cloned (`Error`, `EvalError`, `RangeError`, `ReferenceError`, `SyntaxError`, `TypeError`, `URIError`). The `name` and `message` properties are preserved, and browsers may also preserve `stack` and `cause`. +</Note> + +```javascript +// ✓ These work +worker.postMessage({ + text: 'hello', + numbers: [1, 2, 3], + date: new Date(), + regex: /pattern/g, + binary: new Uint8Array([1, 2, 3]), + map: new Map([['a', 1], ['b', 2]]) +}) + +// ❌ These will throw errors +worker.postMessage({ + fn: () => console.log('hi'), // Functions can't be cloned + element: document.body, // DOM nodes can't be cloned + sym: Symbol('test') // Symbols can't be cloned +}) +``` + +<Warning> +**Performance trap:** Structured cloning can be slow for large objects. If you're passing megabytes of data, consider using Transferable objects instead (covered below). +</Warning> + +### Handling Errors + +Always set up error handlers for workers: + +```javascript +// main.js +const worker = new Worker('worker.js', { type: 'module' }) + +// Handle messages +worker.onmessage = (event) => { + console.log('Result:', event.data) +} + +// Handle errors thrown in the worker +worker.onerror = (event) => { + console.error('Worker error:', event.message) + console.error('File:', event.filename) + console.error('Line:', event.lineno) +} + +// Handle message errors (e.g., data can't be cloned) +worker.onmessageerror = (event) => { + console.error('Message error:', event) +} +``` + +### Using addEventListener (Alternative Syntax) + +You can also use `addEventListener` instead of `onmessage`: + +```javascript +// main.js +const worker = new Worker('worker.js', { type: 'module' }) + +worker.addEventListener('message', (event) => { + console.log('Result:', event.data) +}) + +worker.addEventListener('error', (event) => { + console.error('Error:', event.message) +}) +``` + +```javascript +// worker.js +self.addEventListener('message', (event) => { + const result = processData(event.data) + self.postMessage(result) +}) +``` + +--- + +## Transferable Objects: Zero-Copy Data Transfer + +Copying large amounts of data between threads is slow. For big ArrayBuffers, images, or binary data, use **transferable objects** to move data instead of copying it. + +### The Problem with Copying + +```javascript +// main.js +// Creating a 100MB buffer +const hugeBuffer = new ArrayBuffer(100 * 1024 * 1024) +const array = new Uint8Array(hugeBuffer) + +// Fill it with data +for (let i = 0; i < array.length; i++) { + array[i] = i % 256 +} + +console.time('copy') +worker.postMessage(hugeBuffer) // This COPIES 100MB - slow! +console.timeEnd('copy') // Could take hundreds of milliseconds +``` + +### The Solution: Transfer Ownership + +Instead of copying, you can **transfer** the buffer to the worker. The transfer is nearly instant, but the original becomes unusable: + +```javascript +// main.js +const hugeBuffer = new ArrayBuffer(100 * 1024 * 1024) +const array = new Uint8Array(hugeBuffer) + +// Fill with data... + +console.time('transfer') +// Second argument is an array of objects to transfer +worker.postMessage(hugeBuffer, [hugeBuffer]) +console.timeEnd('transfer') // Nearly instant! + +// WARNING: hugeBuffer is now "detached" (unusable) +console.log(hugeBuffer.byteLength) // 0 +console.log(array.length) // 0 +``` + +```javascript +// worker.js +self.onmessage = (event) => { + const buffer = event.data + console.log(buffer.byteLength) // 104857600 (100MB) + + // Process the data... + const array = new Uint8Array(buffer) + + // Transfer it back when done + self.postMessage(buffer, [buffer]) +} +``` + +### What Can Be Transferred? + +| Transferable Object | Use Case | +|--------------------|-----------| +| [`ArrayBuffer`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer) | Raw binary data | +| [`MessagePort`](https://developer.mozilla.org/en-US/docs/Web/API/MessagePort) | Communication channels | +| [`ImageBitmap`](https://developer.mozilla.org/en-US/docs/Web/API/ImageBitmap) | Image data for canvas | +| [`OffscreenCanvas`](https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas) | Canvas for off-main-thread rendering | +| [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) | Streaming data | +| [`WritableStream`](https://developer.mozilla.org/en-US/docs/Web/API/WritableStream) | Streaming data | +| [`TransformStream`](https://developer.mozilla.org/en-US/docs/Web/API/TransformStream) | Streaming transforms | +| [`AudioData`](https://developer.mozilla.org/en-US/docs/Web/API/AudioData) | Audio processing (WebCodecs) | +| [`VideoFrame`](https://developer.mozilla.org/en-US/docs/Web/API/VideoFrame) | Video processing (WebCodecs) | +| [`RTCDataChannel`](https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel) | WebRTC data channels | + +<Note> +This table shows the most commonly used transferable objects. For a complete list including newer APIs like `MediaStreamTrack` and `WebTransportSendStream`, see MDN's [Transferable objects](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects) documentation. +</Note> + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ COPY VS TRANSFER │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ COPY (Default) TRANSFER │ +│ ───────────── ──────── │ +│ │ +│ Main Thread Worker Thread Main Thread Worker Thread │ +│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ +│ │ [data] │ │ │ │ [data] │ │ │ │ +│ │ 100MB │ │ │ │ 100MB │ │ │ │ +│ └────┬────┘ └─────────┘ └────┬────┘ └─────────┘ │ +│ │ │ │ +│ │ copy │ move │ +│ ▼ ▼ │ +│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ +│ │ [data] │ │ [data] │ │ [empty] │ │ [data] │ │ +│ │ 100MB │ │ 100MB │ │ 0MB │ │ 100MB │ │ +│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ +│ │ +│ • Slow (copies bytes) • Fast (moves pointer) │ +│ • Both have the data • Only one has the data │ +│ • Memory doubled • Memory unchanged │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +<Tip> +**Rule of thumb:** Transfer when the data is large (> 1MB) and you don't need to keep it in the sending context. Copy when the data is small or you need it in both places. +</Tip> + +--- + +## Types of Workers + +There are three types of workers in the browser, each with different purposes. + +### Dedicated Workers + +**[Dedicated Workers](https://developer.mozilla.org/en-US/docs/Web/API/Worker)** are the most common type. They're owned by a single script and can only communicate with that script. + +```javascript +// Only this script can talk to this worker +const worker = new Worker('worker.js', { type: 'module' }) +``` + +Use dedicated workers for: +- Heavy calculations +- Data processing +- Image manipulation +- Any task you want off the main thread + +### Shared Workers + +**[Shared Workers](https://developer.mozilla.org/en-US/docs/Web/API/SharedWorker)** can be accessed by multiple scripts, even across different browser tabs or iframes (as long as they're from the same origin). + +```javascript +// main.js (Tab 1) +const worker = new SharedWorker('shared-worker.js') + +worker.port.onmessage = (event) => { + console.log('Received:', event.data) +} + +worker.port.postMessage('Hello from Tab 1') +``` + +```javascript +// main.js (Tab 2) - connects to the SAME worker +const worker = new SharedWorker('shared-worker.js') + +worker.port.onmessage = (event) => { + console.log('Received:', event.data) +} + +worker.port.postMessage('Hello from Tab 2') +``` + +```javascript +// shared-worker.js +const connections = [] + +self.onconnect = (event) => { + const port = event.ports[0] + connections.push(port) + + port.onmessage = (e) => { + // Broadcast to all connected tabs + connections.forEach(p => { + p.postMessage(`Someone said: ${e.data}`) + }) + } + + port.start() +} +``` + +Use shared workers for: +- Shared state across tabs +- Single WebSocket connection for multiple tabs +- Shared cache or data layer +- Reducing resource usage for identical workers + +<Warning> +Shared Workers have limited browser support. They work in Chrome, Firefox, Edge, and Safari 16+, but are **not supported on Android browsers** (Chrome for Android, Samsung Internet). Check [caniuse.com](https://caniuse.com/sharedworkers) before using in production. +</Warning> + +### Service Workers (Brief Overview) + +**[Service Workers](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API)** are a special type of worker designed for a different purpose: they act as a proxy between your web app and the network. They enable offline functionality, push notifications, and background sync. + +```javascript +// Registering a service worker (in main.js) +if ('serviceWorker' in navigator) { + navigator.serviceWorker.register('/sw.js') + .then(registration => { + console.log('SW registered:', registration) + }) + .catch(error => { + console.log('SW registration failed:', error) + }) +} +``` + +```javascript +// sw.js - intercepts network requests +self.addEventListener('fetch', (event) => { + event.respondWith( + caches.match(event.request) + .then(response => response || fetch(event.request)) + ) +}) +``` + +| Feature | Dedicated Worker | Shared Worker | Service Worker | +|---------|-----------------|---------------|----------------| +| **Purpose** | Background computation | Shared computation | Network proxy, offline | +| **Lifetime** | While page is open | While any tab uses it | Independent of pages | +| **Communication** | `postMessage` | `port.postMessage` | `postMessage` + events | +| **DOM access** | No | No | No | +| **Network intercept** | No | No | Yes | +| **Scope** | Single script | Same-origin scripts | Controlled pages | + +<Note> +Service Workers are a deep topic with their own complexities around lifecycle, caching strategies, and updates. They deserve their own dedicated guide. For now, just know they exist and are different from Web Workers. +</Note> + +--- + +## OffscreenCanvas: Graphics in Workers + +Normally, canvas operations happen on the main thread. With [`OffscreenCanvas`](https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas), you can move rendering to a worker, keeping the main thread free for user interactions. + +### Basic OffscreenCanvas Usage + +```javascript +// main.js +const canvas = document.getElementById('myCanvas') + +// Transfer control to an OffscreenCanvas +const offscreen = canvas.transferControlToOffscreen() + +const worker = new Worker('canvas-worker.js', { type: 'module' }) + +// Transfer the canvas to the worker +worker.postMessage({ canvas: offscreen }, [offscreen]) +``` + +```javascript +// canvas-worker.js +let ctx + +self.onmessage = (event) => { + if (event.data.canvas) { + const canvas = event.data.canvas + ctx = canvas.getContext('2d') + + // Start animation loop in the worker + animate() + } +} + +function animate() { + // Clear canvas + ctx.fillStyle = '#000' + ctx.fillRect(0, 0, 800, 600) + + // Draw something + ctx.fillStyle = '#0f0' + ctx.fillRect( + Math.random() * 700, + Math.random() * 500, + 100, + 100 + ) + + // Request next frame + // Note: requestAnimationFrame is available in dedicated workers only + requestAnimationFrame(animate) +} +``` + +### Real-World Use: Image Processing + +One common use for OffscreenCanvas is image processing: + +```javascript +// main.js +const worker = new Worker('image-worker.js', { type: 'module' }) + +async function processImage(file) { + const bitmap = await createImageBitmap(file) + + worker.postMessage({ + bitmap, + filter: 'grayscale' + }, [bitmap]) // Transfer the bitmap +} + +worker.onmessage = (event) => { + const processedBitmap = event.data.bitmap + + // Draw the result on a visible canvas + const canvas = document.getElementById('result') + const ctx = canvas.getContext('2d') + ctx.drawImage(processedBitmap, 0, 0) +} +``` + +```javascript +// image-worker.js +self.onmessage = async (event) => { + const { bitmap, filter } = event.data + + // Create an OffscreenCanvas matching the image size + const canvas = new OffscreenCanvas(bitmap.width, bitmap.height) + const ctx = canvas.getContext('2d') + + // Draw the image + ctx.drawImage(bitmap, 0, 0) + + // Get pixel data + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height) + const data = imageData.data + + // Apply grayscale filter + if (filter === 'grayscale') { + for (let i = 0; i < data.length; i += 4) { + const avg = (data[i] + data[i + 1] + data[i + 2]) / 3 + data[i] = avg // R + data[i + 1] = avg // G + data[i + 2] = avg // B + // Alpha unchanged + } + } + + // Put processed data back + ctx.putImageData(imageData, 0, 0) + + // Convert to bitmap and send back + const resultBitmap = await createImageBitmap(canvas) + self.postMessage({ bitmap: resultBitmap }, [resultBitmap]) +} +``` + +<Tip> +OffscreenCanvas is great for games, data visualizations, and image/video processing. Anything that involves heavy canvas work can benefit from being moved to a worker. +</Tip> + +--- + +## What Can't Web Workers Do? + +Workers run in a restricted environment. Understanding what they **can't** do is just as important as knowing what they can. + +### No DOM Access + +Workers cannot access the DOM. They can't read or modify HTML elements: + +```javascript +// worker.js +// ❌ All of these will fail +document.getElementById('app') // document is undefined +window.location // window is undefined +document.createElement('div') // Can't create elements +element.addEventListener('click', fn) // Can't add event listeners +``` + +If you need to update the DOM based on worker results, send the data back to the main thread: + +```javascript +// worker.js +const result = heavyCalculation() +self.postMessage({ result }) // Send data to main thread +``` + +```javascript +// main.js +worker.onmessage = (event) => { + // Update DOM on the main thread + document.getElementById('result').textContent = event.data.result +} +``` + +### Different Global Scope + +Workers have their own global object: [`DedicatedWorkerGlobalScope`](https://developer.mozilla.org/en-US/docs/Web/API/DedicatedWorkerGlobalScope). Many familiar globals are missing or different: + +```javascript +// worker.js +console.log(self) // DedicatedWorkerGlobalScope +console.log(window) // undefined +console.log(document) // undefined +console.log(localStorage) // undefined +console.log(sessionStorage) // undefined +console.log(alert) // undefined +``` + +### What Workers CAN Access + +Workers aren't completely isolated. They have access to: + +| Available | Example | +|-----------|---------| +| [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/fetch) | `fetch('/api/data')` | +| [`XMLHttpRequest`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) | Network requests | +| [`setTimeout`](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout) / [`setInterval`](https://developer.mozilla.org/en-US/docs/Web/API/setInterval) | Timers | +| [`IndexedDB`](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) | Database storage | +| [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) | Real-time connections | +| [`crypto`](https://developer.mozilla.org/en-US/docs/Web/API/Crypto) | Cryptographic operations | +| [`navigator`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator) (partial) | `navigator.userAgent`, etc. | +| [`location`](https://developer.mozilla.org/en-US/docs/Web/API/WorkerLocation) (read-only) | URL information | +| [`console`](https://developer.mozilla.org/en-US/docs/Web/API/console) | Logging (appears in DevTools) | +| `importScripts()` | Load scripts (classic workers) | +| `import` / `export` | ES modules (module workers) | + +```javascript +// worker.js - These all work! +console.log('Worker started') + +setTimeout(() => { + console.log('Timer fired in worker') +}, 1000) + +fetch('/api/data') + .then(r => r.json()) + .then(data => { + self.postMessage(data) + }) +``` + +--- + +## Common Mistakes + +### Mistake #1: Trying to Access the DOM + +The most common mistake. It fails silently or throws cryptic errors: + +```javascript +// worker.js +// ❌ WRONG - This won't work +self.onmessage = (event) => { + const result = calculate(event.data) + document.getElementById('output').textContent = result // ERROR! +} + +// ✓ CORRECT - Send data back to main thread +self.onmessage = (event) => { + const result = calculate(event.data) + self.postMessage(result) // Main thread updates the DOM +} +``` + +### Mistake #2: Not Terminating Workers + +Workers consume resources. If you don't terminate them, they keep running: + +```javascript +// main.js +// ❌ WRONG - Creates a new worker for each click, never cleans up +button.addEventListener('click', () => { + const worker = new Worker('worker.js') + worker.postMessage(data) + worker.onmessage = (e) => showResult(e.data) + // Worker keeps running even after we're done! +}) + +// ✓ CORRECT - Terminate when done +button.addEventListener('click', () => { + const worker = new Worker('worker.js') + worker.postMessage(data) + worker.onmessage = (e) => { + showResult(e.data) + worker.terminate() // Clean up! + } +}) + +// ✓ BETTER - Reuse the same worker +const worker = new Worker('worker.js') +worker.onmessage = (e) => showResult(e.data) + +button.addEventListener('click', () => { + worker.postMessage(data) // Reuse existing worker +}) +``` + +### Mistake #3: Overusing Workers for Small Tasks + +Workers have overhead. Creating them, posting messages, and cloning data all take time: + +```javascript +// ❌ WRONG - Worker overhead exceeds computation time +const worker = new Worker('worker.js') +worker.postMessage([1, 2, 3]) // Adding 3 numbers doesn't need a worker + +// ✓ CORRECT - Just do it on the main thread +const sum = [1, 2, 3].reduce((a, b) => a + b, 0) +``` + +<Tip> +**Rule of thumb:** Only use workers for tasks that take more than 50-100ms. For quick operations, the overhead isn't worth it. +</Tip> + +### Mistake #4: Sending Functions to Workers + +Functions can't be cloned: + +```javascript +// ❌ WRONG - Functions can't be sent +worker.postMessage({ + data: [1, 2, 3], + callback: (result) => console.log(result) // ERROR! +}) + +// ✓ CORRECT - Send data, handle callback in onmessage +worker.postMessage({ data: [1, 2, 3] }) +worker.onmessage = (e) => console.log(e.data) // "Callback" on main thread +``` + +### Mistake #5: Forgetting Error Handling + +Workers fail silently if you don't handle errors: + +```javascript +// ❌ WRONG - Errors disappear +const worker = new Worker('worker.js') +worker.postMessage(data) +worker.onmessage = (e) => console.log(e.data) + +// ✓ CORRECT - Always handle errors +const worker = new Worker('worker.js') +worker.postMessage(data) +worker.onmessage = (e) => console.log(e.data) +worker.onerror = (e) => { + console.error('Worker error:', e.message) + console.error('In file:', e.filename, 'line:', e.lineno) +} +``` + +--- + +## Real-World Patterns + +### Pattern 1: Heavy Computation + +Moving CPU-intensive work off the main thread: + +```javascript +// main.js +const worker = new Worker('prime-worker.js', { type: 'module' }) + +document.getElementById('findPrimes').addEventListener('click', () => { + const max = parseInt(document.getElementById('max').value) + + document.getElementById('status').textContent = 'Calculating...' + document.getElementById('findPrimes').disabled = true + + worker.postMessage({ findPrimesUpTo: max }) +}) + +worker.onmessage = (event) => { + const { primes, timeTaken } = event.data + + document.getElementById('status').textContent = + `Found ${primes.length} primes in ${timeTaken}ms` + document.getElementById('findPrimes').disabled = false +} +``` + +```javascript +// prime-worker.js +function isPrime(n) { + if (n < 2) return false + for (let i = 2; i <= Math.sqrt(n); i++) { + if (n % i === 0) return false + } + return true +} + +function findPrimes(max) { + const primes = [] + for (let i = 2; i <= max; i++) { + if (isPrime(i)) primes.push(i) + } + return primes +} + +self.onmessage = (event) => { + const { findPrimesUpTo } = event.data + + const start = performance.now() + const primes = findPrimes(findPrimesUpTo) + const timeTaken = performance.now() - start + + self.postMessage({ primes, timeTaken }) +} +``` + +### Pattern 2: Data Parsing + +Parsing large JSON or CSV files: + +```javascript +// main.js +const worker = new Worker('parser-worker.js', { type: 'module' }) + +async function parseFile(file) { + const text = await file.text() + worker.postMessage({ csv: text }) +} + +worker.onmessage = (event) => { + const { rows, headers, errors } = event.data + console.log(`Parsed ${rows.length} rows`) + displayData(rows) +} + +document.getElementById('fileInput').addEventListener('change', (e) => { + parseFile(e.target.files[0]) +}) +``` + +```javascript +// parser-worker.js +function parseCSV(text) { + const lines = text.split('\n') + const headers = lines[0].split(',').map(h => h.trim()) + const rows = [] + const errors = [] + + for (let i = 1; i < lines.length; i++) { + const line = lines[i].trim() + if (!line) continue + + try { + const values = line.split(',') + const row = {} + headers.forEach((header, index) => { + row[header] = values[index]?.trim() + }) + rows.push(row) + } catch (e) { + errors.push({ line: i, error: e.message }) + } + } + + return { headers, rows, errors } +} + +self.onmessage = (event) => { + const { csv } = event.data + const result = parseCSV(csv) + self.postMessage(result) +} +``` + +### Pattern 3: Real-Time Data Processing + +Processing streaming data (like from WebSocket or sensors): + +```javascript +// main.js +const processingWorker = new Worker('stream-worker.js', { type: 'module' }) +const ws = new WebSocket('wss://data-feed.example.com') + +ws.onmessage = (event) => { + // Don't process on main thread - send to worker + processingWorker.postMessage(JSON.parse(event.data)) +} + +processingWorker.onmessage = (event) => { + // Only update UI with processed results + updateChart(event.data) +} +``` + +```javascript +// stream-worker.js +let buffer = [] +const BATCH_SIZE = 100 + +function processBuffer() { + if (buffer.length < BATCH_SIZE) return + + // Calculate statistics + const values = buffer.map(d => d.value) + const avg = values.reduce((a, b) => a + b, 0) / values.length + const max = Math.max(...values) + const min = Math.min(...values) + + self.postMessage({ avg, max, min, count: buffer.length }) + buffer = [] +} + +self.onmessage = (event) => { + buffer.push(event.data) + processBuffer() +} + +// Process remaining data periodically +setInterval(processBuffer, 1000) +``` + +--- + +## Worker Pools: Reusing Workers + +Creating workers has overhead. For repeated tasks, use a **worker pool** to reuse workers instead of creating new ones: + +```javascript +// WorkerPool.js +export class WorkerPool { + constructor(workerScript, poolSize = navigator.hardwareConcurrency || 4) { + this.workers = [] + this.queue = [] + this.poolSize = poolSize + this.workerScript = workerScript + + // Create workers + for (let i = 0; i < poolSize; i++) { + this.workers.push({ + worker: new Worker(workerScript, { type: 'module' }), + busy: false + }) + } + } + + runTask(data) { + return new Promise((resolve, reject) => { + const task = { data, resolve, reject } + + // Find available worker + const available = this.workers.find(w => !w.busy) + + if (available) { + this.#runOnWorker(available, task) + } else { + // Queue the task + this.queue.push(task) + } + }) + } + + #runOnWorker(workerInfo, task) { + workerInfo.busy = true + + const handleMessage = (event) => { + workerInfo.worker.removeEventListener('message', handleMessage) + workerInfo.busy = false + + task.resolve(event.data) + + // Process queued tasks + if (this.queue.length > 0) { + const nextTask = this.queue.shift() + this.#runOnWorker(workerInfo, nextTask) + } + } + + const handleError = (error) => { + workerInfo.worker.removeEventListener('error', handleError) + workerInfo.busy = false + task.reject(error) + } + + workerInfo.worker.addEventListener('message', handleMessage) + workerInfo.worker.addEventListener('error', handleError) + workerInfo.worker.postMessage(task.data) + } + + terminate() { + this.workers.forEach(w => w.worker.terminate()) + this.workers = [] + this.queue = [] + } +} +``` + +```javascript +// main.js - Using the pool +import { WorkerPool } from './WorkerPool.js' + +const pool = new WorkerPool('compute-worker.js', 4) + +// Process many items in parallel +async function processItems(items) { + const results = await Promise.all( + items.map(item => pool.runTask(item)) + ) + return results +} + +// Example: process 100 items using 4 workers +const items = Array.from({ length: 100 }, (_, i) => ({ id: i, data: Math.random() })) +const results = await processItems(items) +console.log(results) + +// Clean up when done +pool.terminate() +``` + +```javascript +// compute-worker.js +self.onmessage = (event) => { + const { id, data } = event.data + + // Simulate heavy computation + let result = data + for (let i = 0; i < 1000000; i++) { + result = Math.sin(result) * Math.cos(result) + } + + self.postMessage({ id, result }) +} +``` + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ WORKER POOL │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ MAIN THREAD │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ WorkerPool │ │ +│ │ ┌─────────────────────────────────────────────────────────┐ │ │ +│ │ │ TASK QUEUE │ │ │ +│ │ │ [Task 5] [Task 6] [Task 7] [Task 8] ... │ │ │ +│ │ └─────────────────────────────────────────────────────────┘ │ │ +│ └──────────────┬───────────────┬───────────────┬───────────────┬───┘ │ +│ │ │ │ │ │ +│ ▼ ▼ ▼ ▼ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ Worker 1 │ │ Worker 2 │ │ Worker 3 │ │ Worker 4 │ │ +│ │ [Task 1] │ │ [Task 2] │ │ [Task 3] │ │ [Task 4] │ │ +│ │ (busy) │ │ (busy) │ │ (busy) │ │ (busy) │ │ +│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ +│ │ +│ • Reuses existing workers (no creation overhead) │ +│ • Tasks queue when all workers are busy │ +│ • Automatically assigns tasks as workers become free │ +│ • Pool size often matches CPU core count │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +<Tip> +`navigator.hardwareConcurrency` returns the number of logical CPU cores. Using this as your pool size lets you maximize parallelism without oversubscribing. +</Tip> + +--- + +## Inline Workers: The Blob URL Trick + +Sometimes you want a worker without a separate file. You can create workers from strings using [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) URLs: + +```javascript +// Create a worker from a string (no separate file needed!) +function createWorkerFromString(code) { + const blob = new Blob([code], { type: 'application/javascript' }) + const url = URL.createObjectURL(blob) + const worker = new Worker(url) + + // The URL can be revoked immediately after the worker is created. + // The browser keeps the blob data until the worker finishes loading. + URL.revokeObjectURL(url) + + return worker +} + +// Usage +const workerCode = ` + self.onmessage = (event) => { + const numbers = event.data + const sum = numbers.reduce((a, b) => a + b, 0) + self.postMessage(sum) + } +` + +const worker = createWorkerFromString(workerCode) +worker.postMessage([1, 2, 3, 4, 5]) +worker.onmessage = (e) => console.log('Sum:', e.data) // Sum: 15 +``` + +### A Cleaner Pattern: Function-Based Workers + +You can even define the worker logic as a function: + +```javascript +function createWorkerFromFunction(fn) { + // Convert function to string and wrap in self.onmessage + const code = ` + const workerFn = ${fn.toString()} + self.onmessage = (event) => { + const result = workerFn(event.data) + self.postMessage(result) + } + ` + + const blob = new Blob([code], { type: 'application/javascript' }) + const url = URL.createObjectURL(blob) + return new Worker(url) +} + +// Usage - define worker logic as a normal function! +const worker = createWorkerFromFunction((data) => { + // This runs in the worker + return data.map(n => n * 2) +}) + +worker.postMessage([1, 2, 3]) +worker.onmessage = (e) => console.log(e.data) // [2, 4, 6] +``` + +<Warning> +**Limitations of inline workers:** +- The function can't use closures (no access to outer scope) +- Can't import modules (the code is a string) +- Harder to debug (no source maps) +- Best used for simple, self-contained tasks +</Warning> + +--- + +## Key Takeaways + +<Info> +**The key things to remember:** + +1. **Web Workers provide true parallelism** — Unlike async/await (which is concurrent but single-threaded), workers run on separate CPU threads simultaneously. + +2. **Use workers for CPU-bound tasks** — Async is for waiting (network, timers). Workers are for computing (heavy calculations, data processing). + +3. **Workers communicate via `postMessage`** — Data is copied by default using the structured clone algorithm. Workers can't directly access main thread variables. + +4. **Workers can't touch the DOM** — No `document`, no `window`, no `localStorage`. If you need to update the UI, send data back to the main thread. + +5. **Transfer large data instead of copying** — For big ArrayBuffers, use `postMessage(data, [data])` to transfer ownership. The transfer is nearly instant. + +6. **Module workers are the modern approach** — Use `new Worker('file.js', { type: 'module' })` to enable `import`/`export` syntax and modern features. + +7. **Three types of workers exist** — Dedicated (one owner), Shared (multiple tabs), and Service Workers (network proxy). Use Dedicated for most cases. + +8. **Always terminate workers when done** — Call `worker.terminate()` or they'll keep running and consuming resources. + +9. **Don't overuse workers for small tasks** — Worker creation and message passing have overhead. Only use them for tasks taking 50ms+. + +10. **Worker pools improve performance** — Reuse workers instead of creating new ones for repeated tasks. Match pool size to CPU cores. +</Info> + +--- + +## Test Your Knowledge + +<AccordionGroup> + <Accordion title="Question 1: What's the difference between async/await and Web Workers?"> + **Answer:** + + Async/await provides **concurrency** on a single thread. When you `await`, JavaScript pauses that function and runs other code, but everything still runs on one thread, taking turns. + + Web Workers provide **parallelism** on multiple threads. A worker runs on a completely separate thread, executing simultaneously with the main thread. + + ```javascript + // Async: Takes turns on one thread + async function fetchData() { + await fetch('/api') // Pauses here, other code can run + } + + // Workers: Actually runs at the same time + const worker = new Worker('heavy-task.js') + worker.postMessage(data) // Worker computes in parallel + // Main thread continues immediately + ``` + + Use async for I/O-bound tasks (network, files). Use workers for CPU-bound tasks (calculations, processing). + </Accordion> + + <Accordion title="Question 2: Why can't workers access the DOM?"> + **Answer:** + + The DOM is not thread-safe. If multiple threads could modify the DOM simultaneously, you'd get race conditions and corrupted state. Browsers would need complex locking mechanisms. + + Instead, browsers made a design choice: only the main thread can touch the DOM. Workers do computation and send results back: + + ```javascript + // worker.js + // ❌ Can't do this + document.getElementById('result').textContent = 'Done' + + // ✓ Send data back instead + self.postMessage({ result: 'Done' }) + + // main.js + worker.onmessage = (e) => { + document.getElementById('result').textContent = e.data.result + } + ``` + + This constraint keeps things simple and bug-free. + </Accordion> + + <Accordion title="Question 3: When should you use transferable objects?"> + **Answer:** + + Use transferable objects when: + 1. You're sending large data (> 1MB) + 2. You don't need to keep the data in the sending context + + ```javascript + // Large buffer (100MB) + const buffer = new ArrayBuffer(100 * 1024 * 1024) + + // ❌ SLOW: Copies 100MB + worker.postMessage(buffer) + + // ✓ FAST: Transfers ownership instantly + worker.postMessage(buffer, [buffer]) + // buffer is now empty (byteLength = 0) + ``` + + Transferable objects include: ArrayBuffer, MessagePort, ImageBitmap, OffscreenCanvas, and various streams. + </Accordion> + + <Accordion title="Question 4: What's the difference between Dedicated and Shared Workers?"> + **Answer:** + + **Dedicated Workers** belong to a single script. Only that script can communicate with them. + + ```javascript + const worker = new Worker('worker.js') // Only this script uses it + ``` + + **Shared Workers** can be accessed by multiple scripts, even across different tabs of the same origin. + + ```javascript + // Tab 1 and Tab 2 both connect to the same worker + const worker = new SharedWorker('shared.js') + worker.port.postMessage('hello') + ``` + + Use Shared Workers for: + - Shared state across tabs + - Single WebSocket connection for multiple tabs + - Reducing memory by sharing one worker instance + + Note: Shared Workers have limited browser support (not in Safari). + </Accordion> + + <Accordion title="Question 5: How do you create a worker without a separate file?"> + **Answer:** + + Use a Blob URL to create a worker from a string: + + ```javascript + const code = ` + self.onmessage = (event) => { + const result = event.data * 2 + self.postMessage(result) + } + ` + + const blob = new Blob([code], { type: 'application/javascript' }) + const url = URL.createObjectURL(blob) + const worker = new Worker(url) + + worker.postMessage(5) + worker.onmessage = (e) => console.log(e.data) // 10 + + // Clean up + URL.revokeObjectURL(url) + ``` + + This is useful for simple tasks or demos, but has limitations: no imports, no closures, harder to debug. + </Accordion> + + <Accordion title="Question 6: What happens if you forget to terminate a worker?"> + **Answer:** + + The worker keeps running and consuming resources (memory, CPU time). If you create workers in a loop or on repeated events without terminating them, you'll leak resources: + + ```javascript + // ❌ Memory leak: creates new worker every click + button.onclick = () => { + const worker = new Worker('task.js') + worker.postMessage(data) + worker.onmessage = (e) => showResult(e.data) + // Worker never terminated! + } + + // ✓ Fixed: terminate after use + button.onclick = () => { + const worker = new Worker('task.js') + worker.postMessage(data) + worker.onmessage = (e) => { + showResult(e.data) + worker.terminate() // Clean up + } + } + + // ✓ Better: reuse one worker + const worker = new Worker('task.js') + worker.onmessage = (e) => showResult(e.data) + + button.onclick = () => { + worker.postMessage(data) // Reuse + } + ``` + </Accordion> +</AccordionGroup> + +--- + +## Related Concepts + +<CardGroup cols={2}> + <Card title="Event Loop" icon="arrows-spin" href="/concepts/event-loop"> + How JavaScript handles async on a single thread. Workers bypass this limitation. + </Card> + <Card title="Promises" icon="handshake" href="/concepts/promises"> + The foundation of async JavaScript. Workers use postMessage, not Promises, for communication. + </Card> + <Card title="async/await" icon="hourglass" href="/concepts/async-await"> + Modern async syntax. Great for I/O, but workers handle CPU-bound tasks better. + </Card> + <Card title="Callbacks" icon="phone" href="/concepts/callbacks"> + The original async pattern. Workers use message callbacks for communication. + </Card> +</CardGroup> + +--- + +## Reference + +<CardGroup cols={2}> + <Card title="Web Workers API — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API"> + Official MDN reference for the Web Workers API. + </Card> + <Card title="Using Web Workers — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers"> + Comprehensive guide to creating and using Web Workers. + </Card> + <Card title="Worker — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/Worker"> + API reference for the Worker constructor and methods. + </Card> + <Card title="Structured Clone Algorithm — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm"> + How data is copied when using postMessage. + </Card> +</CardGroup> + +## Articles + +<CardGroup cols={2}> + <Card title="How Fast Are Web Workers? — Mozilla Hacks" icon="newspaper" href="https://hacks.mozilla.org/2015/07/how-fast-are-web-workers/"> + Performance analysis from Mozilla engineers. Answers "is the overhead worth it?" with real benchmarks. + </Card> + <Card title="Threading the Web with Module Workers — web.dev" icon="newspaper" href="https://web.dev/articles/module-workers"> + Deep dive into module workers with `type: 'module'`. Essential reading for modern worker patterns. + </Card> + <Card title="Using Web Workers for Safe Concurrent JavaScript — Smashing Magazine" icon="newspaper" href="https://www.smashingmagazine.com/2021/06/web-workers-2021/"> + Real-world examples including image processing and physics simulations. Great for seeing workers in action. + </Card> + <Card title="Comlink — Google Chrome Labs" icon="newspaper" href="https://github.com/GoogleChromeLabs/comlink"> + Library that makes workers feel like async functions. Eliminates postMessage boilerplate entirely. + </Card> +</CardGroup> + +## Videos + +<CardGroup cols={2}> + <Card title="Web Workers Explained — Web Dev Simplified" icon="video" href="https://www.youtube.com/watch?v=Gcp7triXFjg"> + Beginner-friendly 12-minute introduction. Perfect starting point if you've never used workers. + </Card> + <Card title="JavaScript Web Workers — Fireship" icon="video" href="https://www.youtube.com/watch?v=EiPytIxrZtU"> + Fast-paced 100-second overview. Great for quick refresher or deciding if you need workers. + </Card> + <Card title="Web Workers Crash Course — Traversy Media" icon="video" href="https://www.youtube.com/watch?v=tPwkKF8WAXs"> + Practical 30-minute deep dive with a real example. Shows the full workflow from problem to solution. + </Card> +</CardGroup> diff --git a/docs/contributing.mdx b/docs/contributing.mdx new file mode 100644 index 00000000..798fb77b --- /dev/null +++ b/docs/contributing.mdx @@ -0,0 +1,91 @@ +--- +title: "Contributing" +description: "How to contribute to 33 JavaScript Concepts" +--- + +## Welcome Contributors! + +This project would not be possible without your help and support, and we appreciate your willingness to contribute! + +<Info> +By contributing, you agree that your contributions will be licensed under the [MIT license](https://github.com/leonardomso/33-js-concepts/blob/master/LICENSE). +</Info> + +## How to Contribute + +<Steps> + <Step title="Fork the Repository"> + Start by forking the [main repository](https://github.com/leonardomso/33-js-concepts) to your GitHub account. + </Step> + <Step title="Make Your Changes"> + Add new resources, fix broken links, or improve existing content. + </Step> + <Step title="Submit a Pull Request"> + Create a pull request with a clear description of your changes. + </Step> +</Steps> + +## Adding New Resources + +When adding new resources, please follow these guidelines: + +<AccordionGroup> + <Accordion title="Resource Quality"> + - Resources should be high-quality and educational + - Content should be accurate and up-to-date + - Prefer resources from reputable sources + </Accordion> + <Accordion title="Format"> + Include the author name in the link text: + ```markdown + - [Article Title — Author Name](URL) + ``` + </Accordion> + <Accordion title="Categories"> + Place resources in the appropriate category: + - **Reference**: Official documentation (MDN, ECMAScript spec) + - **Articles**: Blog posts and tutorials + - **Videos**: YouTube tutorials and conference talks + - **Books**: Published books and free online books + </Accordion> +</AccordionGroup> + +## Creating Translations + +We welcome translations to make this resource accessible to developers worldwide! + +<Steps> + <Step title="Fork the Repository"> + Fork the [main repository](https://github.com/leonardomso/33-js-concepts) to your account. + </Step> + <Step title="Add Yourself to Watch List"> + Stay updated with changes by watching the main repository. + </Step> + <Step title="Translate the Content"> + Translate the content in your forked repository. + </Step> + <Step title="Update the README"> + Add a link to your translation in the Community section: + ```markdown + - [Your language (English name)](link-to-your-repo) — Your Name + ``` + </Step> + <Step title="Submit a PR"> + Create a Pull Request with the title "Add [language] translation" + </Step> +</Steps> + +## Code of Conduct + +Please read our [Code of Conduct](https://github.com/leonardomso/33-js-concepts/blob/master/CODE_OF_CONDUCT.md) before contributing. We are committed to providing a welcoming and inclusive environment for all contributors. + +## Questions? + +If you have any questions, feel free to: + +- Open an issue on [GitHub](https://github.com/leonardomso/33-js-concepts/issues) +- Reach out to the maintainer [@leonardomso](https://github.com/leonardomso) + +<Card title="View on GitHub" icon="github" href="https://github.com/leonardomso/33-js-concepts"> + Check out the repository and start contributing today! +</Card> diff --git a/docs/docs.json b/docs/docs.json new file mode 100644 index 00000000..272b598a --- /dev/null +++ b/docs/docs.json @@ -0,0 +1,134 @@ +{ + "$schema": "https://mintlify.com/docs.json", + "theme": "maple", + "name": "33 JavaScript Concepts", + "description": "33 concepts every JavaScript developer should know", + "colors": { + "primary": "#F7DF1E", + "light": "#FAFAFA", + "dark": "#1A1A1A" + }, + "favicon": "/favicon.svg", + "navbar": { + "links": [ + { + "label": "GitHub", + "href": "https://github.com/leonardomso/33-js-concepts" + } + ] + }, + "navigation": { + "tabs": [ + { + "tab": "Learn", + "groups": [ + { + "group": "Getting Started", + "icon": "rocket", + "pages": [ + "index", + "getting-started/about", + "getting-started/how-to-learn", + "getting-started/prerequisites", + "getting-started/learning-paths" + ] + }, + { + "group": "Fundamentals", + "icon": "cube", + "pages": [ + "concepts/primitive-types", + "concepts/value-reference-types", + "concepts/type-coercion", + "concepts/equality-operators", + "concepts/scope-and-closures", + "concepts/call-stack" + ] + }, + { + "group": "Functions & Execution", + "icon": "code", + "pages": [ + "concepts/event-loop", + "concepts/iife-modules" + ] + }, +{ + "group": "Web Platform", + "icon": "browser", + "pages": [ + "concepts/dom", + "concepts/http-fetch", + "concepts/web-workers" + ] + }, + { + "group": "Object-Oriented JS", + "icon": "sitemap", + "pages": [ + "concepts/factories-classes", + "concepts/this-call-apply-bind", + "concepts/object-creation-prototypes", + "concepts/inheritance-polymorphism" + ] + }, + { + "group": "Async JavaScript", + "icon": "clock", + "pages": [ + "concepts/callbacks", + "concepts/promises", + "concepts/async-await", + "concepts/generators-iterators" + ] + }, + { + "group": "Functional Programming", + "icon": "filter", + "pages": [ + "concepts/higher-order-functions", + "concepts/pure-functions", + "concepts/map-reduce-filter", + "concepts/recursion", + "concepts/currying-composition" + ] + }, + { + "group": "Advanced Topics", + "icon": "graduation-cap", + "pages": [ + "concepts/javascript-engines", + "concepts/error-handling", + "concepts/regular-expressions", + "concepts/modern-js-syntax", + "concepts/es-modules", + "concepts/data-structures", + "concepts/algorithms-big-o", + "concepts/design-patterns", + "concepts/clean-code" + ] + } + ] + }, + { + "tab": "Community", + "groups": [ + { + "group": "Get Involved", + "icon": "users", + "pages": [ + "contributing", + "translations" + ] + } + ] + } + ] + }, + "footer": { + "socials": { + "github": "https://github.com/leonardomso/33-js-concepts", + "x": "https://x.com/leonardomso" + } + } +} diff --git a/docs/getting-started/about.mdx b/docs/getting-started/about.mdx new file mode 100644 index 00000000..9bc59603 --- /dev/null +++ b/docs/getting-started/about.mdx @@ -0,0 +1,173 @@ +--- +title: "About 33 JavaScript Concepts: Origin and Goals" +sidebarTitle: "What is This Project?" +description: "Discover the story behind 33 JavaScript Concepts. Learn what topics are covered, who this guide is for, and how it helps you become a better developer." +--- + +## The Origin Story + +In 2017, Stephen Curtis wrote an article titled ["33 Fundamentals Every JavaScript Developer Should Know"](https://medium.com/@stephenthecurt/33-fundamentals-every-javascript-developer-should-know-13dd720a90d1). It outlined the core concepts that separate developers who *use* JavaScript from developers who truly *understand* it. + +[Leonardo Maldonado](https://github.com/leonardomso) took this idea and built something bigger: a curated collection of the best resources for each concept. What started as a personal learning project became one of the most popular JavaScript repositories on GitHub. + +<Tip> +**Recognition:** GitHub featured this project as one of the [top open source projects of 2018](https://github.blog/news-insights/octoverse/new-open-source-projects/#top-projects-of-2018). +</Tip> + +--- + +## Who Is This For? + +This guide is for anyone who wants to learn JavaScript, regardless of your current level. + +| If you are... | This guide will help you... | +|---------------|---------------------------| +| **A complete beginner** | Build a solid foundation from the ground up | +| **Self-taught** | Fill gaps in your knowledge | +| **Preparing for interviews** | Understand concepts interviewers commonly ask about | +| **An experienced developer** | Deepen your understanding of how JavaScript works | + +There are no prerequisites. If you've never written a line of code, you can start here. + +--- + +## The Original 33 Concepts + +These are the original 33 concepts that inspired this project. We've since reorganized and expanded some topics, but this is the foundation: + +<AccordionGroup> + <Accordion title="Fundamentals (Concepts 1-6)"> + 1. **Call Stack** - How JavaScript tracks function execution + 2. **Primitive Types** - String, Number, Boolean, Null, Undefined, Symbol, BigInt + 3. **Value Types vs Reference Types** - How data is stored and passed + 4. **Type Coercion** - Implicit and explicit type conversion + 5. **Equality Operators** - == vs === and how comparisons work + 6. **Scope and Closures** - Where variables are accessible and how functions remember their environment + </Accordion> + + <Accordion title="Functions & Execution (Concepts 7-10)"> + 7. **Expression vs Statement** - Understanding the difference + 8. **IIFE, Modules, and Namespaces** - Code organization patterns + 9. **Message Queue and Event Loop** - JavaScript's concurrency model + 10. **Timers** - setTimeout, setInterval, and requestAnimationFrame + </Accordion> + + <Accordion title="JavaScript Engines (Concepts 11-13)"> + 11. **JavaScript Engines** - V8, SpiderMonkey, and how JS runs + 12. **Bitwise Operators** - Low-level operations and typed arrays + 13. **DOM and Layout Trees** - How browsers render pages + </Accordion> + + <Accordion title="Object-Oriented JavaScript (Concepts 14-18)"> + 14. **Factories and Classes** - Object creation patterns + 15. **this, call, apply, and bind** - Context and function binding + 16. **new, Constructor, instanceof** - Object instantiation + 17. **Prototype Inheritance** - JavaScript's inheritance model + 18. **Object.create and Object.assign** - Object manipulation methods + </Accordion> + + <Accordion title="Functional Programming (Concepts 19-23)"> + 19. **map, reduce, filter** - Array transformation methods + 20. **Pure Functions and Side Effects** - Functional programming basics + 21. **Closures** - Functions that remember their scope + 22. **Higher-Order Functions** - Functions that operate on functions + 23. **Recursion** - Functions that call themselves + </Accordion> + + <Accordion title="Async JavaScript (Concepts 24-26)"> + 24. **Collections and Generators** - Iterables and lazy evaluation + 25. **Promises** - Handling asynchronous operations + 26. **async/await** - Modern async syntax + </Accordion> + + <Accordion title="Advanced Topics (Concepts 27-33)"> + 27. **Data Structures** - Arrays, Objects, Maps, Sets, and more + 28. **Big O Notation** - Algorithm complexity analysis + 29. **Algorithms** - Common algorithms in JavaScript + 30. **Inheritance and Polymorphism** - OOP principles + 31. **Design Patterns** - Proven solutions to common problems + 32. **Currying and Composition** - Advanced functional techniques + 33. **Clean Code** - Writing maintainable JavaScript + </Accordion> +</AccordionGroup> + +--- + +## What We've Changed + +JavaScript and web development have evolved since the original list was created. We've updated this guide to better reflect what modern developers need to know. + +### Concepts We Added + +| Concept | Why We Added It | +|---------|-----------------| +| **Callbacks** | Essential for understanding async JavaScript before diving into Promises | +| **HTTP and Fetch** | Every web developer needs to know how to make network requests | +| **Web Workers** | Important for performance and running code off the main thread | +| **Error Handling** | Critical for building reliable applications | +| **Regular Expressions** | A fundamental tool for text processing and validation | +| **Modern JS Syntax** | Destructuring, spread operator, and other ES6+ features are now standard | +| **ES Modules** | The official module system for JavaScript | + +### Concepts We Removed or Merged + +| Original Concept | What Happened | +|------------------|---------------| +| **Expression vs Statement** | Covered within other concept pages where relevant | +| **Timers** | Merged into the Event Loop concept | +| **Bitwise Operators** | Rarely used in day-to-day JavaScript development | +| **new, Constructor, instanceof** | Merged into Factories and Classes | +| **Object.create and Object.assign** | Merged into Object Creation and Prototypes | + +<Info> +The goal isn't to have exactly 33 concepts. It's to give you the knowledge you need to truly understand JavaScript. +</Info> + +--- + +## What Makes This Guide Different? + +### Learn the Concept, Then Go Deeper + +Each concept page teaches you the topic directly with clear explanations and practical code examples. Once you understand the fundamentals, you'll find a curated list of articles, videos, and books to explore further. + +### Curated Resources + +Every resource is hand-picked from across the web. Instead of one perspective, you get the best explanations from multiple teachers and sources. + +### Community-Driven + +Hundreds of developers have contributed to this project. Resources are continuously reviewed, updated, and improved by the community. + +### Multiple Formats + +Everyone learns differently. Each concept includes: + +<CardGroup cols={3}> + <Card title="Articles" icon="newspaper"> + In-depth written explanations + </Card> + <Card title="Videos" icon="video"> + Visual explanations and talks + </Card> + <Card title="Books" icon="book"> + Comprehensive deep-dives + </Card> +</CardGroup> + +### Available in 40+ Languages + +Thanks to our community of translators, this guide is accessible to developers worldwide. Check out the [translations page](/translations) to find your language. + +--- + +## Ready to Continue? + +<CardGroup cols={2}> + <Card title="How to Learn" icon="book-open" href="/getting-started/how-to-learn"> + Learn how to use this guide effectively + </Card> + <Card title="Prerequisites" icon="wrench" href="/getting-started/prerequisites"> + Set up your learning environment + </Card> +</CardGroup> diff --git a/docs/getting-started/how-to-learn.mdx b/docs/getting-started/how-to-learn.mdx new file mode 100644 index 00000000..4196899b --- /dev/null +++ b/docs/getting-started/how-to-learn.mdx @@ -0,0 +1,172 @@ +--- +title: "How to Learn JavaScript Effectively with This Guide" +sidebarTitle: "How to Learn" +description: "Learn how to study JavaScript effectively. Tips for practicing code, understanding concepts, and getting the most from each lesson in this guide." +--- + +## How Each Concept Page Works + +Every concept page in this guide follows a consistent structure to help you learn effectively: + +<Steps> + <Step title="Overview"> + Each page starts with a clear explanation of the concept. We break down what it is, why it matters, and how it works in JavaScript. + </Step> + <Step title="Code Examples"> + You'll find practical code examples that demonstrate the concept. Run these in your browser's console or code editor to see them in action. + </Step> + <Step title="Common Mistakes"> + We highlight the mistakes developers commonly make so you can avoid them. + </Step> + <Step title="Key Takeaways"> + A summary of the most important points to remember. + </Step> + <Step title="Curated Resources"> + Hand-picked articles, videos, and book recommendations for deeper learning. + </Step> +</Steps> + +--- + +## Types of Resources + +Each concept includes multiple types of learning materials. Choose what works best for your learning style: + +<CardGroup cols={3}> + <Card title="Articles" icon="newspaper"> + **Best for:** Deep understanding + + Written tutorials and explanations you can read at your own pace. Great for reference. + </Card> + <Card title="Videos" icon="video"> + **Best for:** Visual learners + + Watch concepts explained visually. Many include animations and live coding. + </Card> + <Card title="Books" icon="book"> + **Best for:** Comprehensive learning + + In-depth coverage for when you want to go deep on a topic. + </Card> +</CardGroup> + +<Tip> +**Mix it up.** If an article doesn't click, try watching a video on the same topic. Different explanations work for different people. +</Tip> + +--- + +## Tips for Effective Learning + +### 1. Don't Just Read - Practice + +Reading about JavaScript isn't enough. You need to write code. + +```javascript +// Don't just read this example - type it yourself +const numbers = [1, 2, 3, 4, 5] +const doubled = numbers.map(num => num * 2) +console.log(doubled) // [2, 4, 6, 8, 10] +``` + +Open your browser's console (press F12) or use a code editor and actually run the examples. Modify them. Break them. See what happens. + +### 2. Take Your Time + +This isn't a race. Some concepts will click immediately. Others might take days or weeks to fully understand. That's normal. + +| Concept Type | Typical Time to Understand | +|--------------|---------------------------| +| Basic syntax | Hours | +| Core concepts (scope, closures) | Days to weeks | +| Advanced patterns | Weeks to months | + +### 3. Follow the Order (Especially for Beginners) + +The concepts build on each other. If you're new to JavaScript, start from the beginning: + +1. **Primitive Types** - What are the basic building blocks? +2. **Value vs Reference Types** - How is data stored? +3. **Scope and Closures** - Where can you access variables? +4. **Call Stack** - How does JavaScript execute code? + +Jumping ahead might leave gaps in your understanding. + +### 4. Revisit Concepts + +You won't master a concept in one sitting. Plan to revisit: + +<Steps> + <Step title="First Pass"> + Read the overview and try the basic examples + </Step> + <Step title="Second Pass (1 week later)"> + Explore the curated resources. Watch a video or read an article. + </Step> + <Step title="Third Pass (1 month later)"> + Review and apply the concept in a real project + </Step> +</Steps> + +### 5. Explain It to Someone Else + +The best way to know if you understand something is to explain it. Try: + +- Writing a blog post about a concept you learned +- Explaining it to a friend or colleague +- Answering questions on Stack Overflow or Reddit + +If you can't explain it simply, you don't understand it well enough yet. + +--- + +## How Much Time Should You Spend? + +There's no "right" answer, but here are some guidelines: + +| Your Goal | Suggested Pace | +|-----------|---------------| +| Casual learning | 1 concept per week | +| Active study | 2-3 concepts per week | +| Interview prep | 1 concept per day (review mode) | + +<Tip> +**Quality over quantity.** It's better to deeply understand 5 concepts than to skim through all 33. +</Tip> + +--- + +## Using the Browser Console + +The fastest way to practice is with your browser's built-in console: + +<Steps> + <Step title="Open DevTools"> + Press **F12** (or **Cmd+Option+J** on Mac) in any browser + </Step> + <Step title="Go to Console Tab"> + Click the "Console" tab + </Step> + <Step title="Type JavaScript"> + Type any JavaScript code and press Enter to run it + </Step> +</Steps> + +```javascript +// Try this in your console right now +const greeting = "Hello, JavaScript!" +console.log(greeting) +``` + +--- + +## Ready to Set Up? + +<CardGroup cols={2}> + <Card title="Prerequisites" icon="wrench" href="/getting-started/prerequisites"> + Get the tools you need to start learning + </Card> + <Card title="Learning Paths" icon="map" href="/getting-started/learning-paths"> + Find the right path for your experience level + </Card> +</CardGroup> diff --git a/docs/getting-started/learning-paths.mdx b/docs/getting-started/learning-paths.mdx new file mode 100644 index 00000000..f0185258 --- /dev/null +++ b/docs/getting-started/learning-paths.mdx @@ -0,0 +1,239 @@ +--- +title: "JavaScript Learning Paths: Beginner to Advanced" +sidebarTitle: "Learning Paths" +description: "Find the right JavaScript learning path for your level. Structured guides for beginners, intermediate developers, and technical interview preparation." +--- + +## Choose Your Path + +Not everyone starts from the same place. Choose a learning path that matches your experience and goals. + +<Tabs> + <Tab title="Beginner"> + **For:** Complete beginners or those new to JavaScript + + **Time:** 4-8 weeks at a comfortable pace + + Start here if you're new to programming or just starting with JavaScript. + </Tab> + <Tab title="Intermediate"> + **For:** Developers with some JavaScript experience + + **Time:** 2-4 weeks + + Choose this if you can write basic JavaScript but want to understand it more deeply. + </Tab> + <Tab title="Interview Prep"> + **For:** Preparing for technical interviews + + **Time:** 1-2 weeks (review mode) + + Focus on concepts commonly asked in JavaScript interviews. + </Tab> +</Tabs> + +--- + +## Beginner Path + +If you're new to JavaScript, follow this order. Each concept builds on the previous ones. + +<Steps> + <Step title="Week 1-2: The Fundamentals"> + Start with the building blocks of JavaScript. + + 1. [Primitive Types](/concepts/primitive-types) - What types of data exist in JavaScript? + 2. [Value vs Reference Types](/concepts/value-reference-types) - How is data stored and copied? + 3. [Type Coercion](/concepts/type-coercion) - How JavaScript converts between types + 4. [Equality Operators](/concepts/equality-operators) - The difference between == and === + </Step> + + <Step title="Week 3-4: Scope and Functions"> + Understand how JavaScript organizes and executes code. + + 5. [Scope and Closures](/concepts/scope-and-closures) - Where variables are accessible + 6. [Call Stack](/concepts/call-stack) - How JavaScript tracks function calls + 7. [Event Loop](/concepts/event-loop) - How async code works + </Step> + + <Step title="Week 5-6: Working with Data"> + Learn to transform and manipulate data. + + 8. [Higher-Order Functions](/concepts/higher-order-functions) - Functions that work with functions + 9. [map, reduce, filter](/concepts/map-reduce-filter) - Essential array methods + 10. [Pure Functions](/concepts/pure-functions) - Writing predictable code + </Step> + + <Step title="Week 7-8: Async JavaScript"> + Handle operations that take time. + + 11. [Callbacks](/concepts/callbacks) - The original async pattern + 12. [Promises](/concepts/promises) - Modern async handling + 13. [async/await](/concepts/async-await) - Clean async syntax + </Step> +</Steps> + +<Info> +**Take your time.** There's no rush. If a concept doesn't click, spend more time on it before moving on. Revisit the resources, try different explanations, and practice with code. +</Info> + +--- + +## Intermediate Path + +You know JavaScript basics. Now deepen your understanding with these concepts: + +<Steps> + <Step title="How JavaScript Works"> + Understand what's happening under the hood. + + 1. [Call Stack](/concepts/call-stack) - How function execution is tracked + 2. [Event Loop](/concepts/event-loop) - The concurrency model + 3. [JavaScript Engines](/concepts/javascript-engines) - V8 and how code runs + </Step> + + <Step title="Object-Oriented JavaScript"> + Master objects and prototypes. + + 4. [this, call, apply, bind](/concepts/this-call-apply-bind) - Context binding + 5. [Object Creation and Prototypes](/concepts/object-creation-prototypes) - The prototype chain + 6. [Factories and Classes](/concepts/factories-classes) - Object creation patterns + 7. [Inheritance and Polymorphism](/concepts/inheritance-polymorphism) - OOP in JavaScript + </Step> + + <Step title="Functional Programming"> + Write cleaner, more predictable code. + + 8. [Pure Functions](/concepts/pure-functions) - Side-effect free functions + 9. [Higher-Order Functions](/concepts/higher-order-functions) - Functions as values + 10. [Currying and Composition](/concepts/currying-composition) - Advanced patterns + 11. [Recursion](/concepts/recursion) - Functions that call themselves + </Step> + + <Step title="Advanced Patterns"> + Level up your code quality. + + 12. [Design Patterns](/concepts/design-patterns) - Proven solutions + 13. [Error Handling](/concepts/error-handling) - Graceful failure + 14. [Clean Code](/concepts/clean-code) - Writing maintainable code + </Step> +</Steps> + +--- + +## Interview Prep Path + +Technical interviews often focus on these concepts. Make sure you can explain them clearly and write code examples. + +### Must-Know Concepts + +These come up in almost every JavaScript interview: + +| Concept | Why It's Asked | Key Things to Know | +|---------|---------------|-------------------| +| [Closures](/concepts/scope-and-closures) | Tests fundamental understanding | How inner functions access outer variables | +| [this keyword](/concepts/this-call-apply-bind) | Common source of bugs | The four binding rules | +| [Promises](/concepts/promises) | Essential for async code | Chaining, error handling, Promise.all | +| [Event Loop](/concepts/event-loop) | Shows deep understanding | Call stack, task queue, microtasks | +| [Prototypes](/concepts/object-creation-prototypes) | JavaScript's inheritance | Prototype chain, Object.create | + +### Common Interview Questions by Topic + +<AccordionGroup> + <Accordion title="Scope and Closures"> + - What is a closure? Give an example. + - What's the difference between `var`, `let`, and `const`? + - Explain lexical scope. + - What is hoisting? + + **Study:** [Scope and Closures](/concepts/scope-and-closures) + </Accordion> + + <Accordion title="this Keyword"> + - What are the rules for `this` binding? + - What's the difference between `call`, `apply`, and `bind`? + - How does `this` work in arrow functions? + - What's the output of [tricky this code]? + + **Study:** [this, call, apply, bind](/concepts/this-call-apply-bind) + </Accordion> + + <Accordion title="Async JavaScript"> + - What's the difference between callbacks, promises, and async/await? + - How does the event loop work? + - What are microtasks vs macrotasks? + - How do you handle errors in async code? + + **Study:** [Promises](/concepts/promises), [async/await](/concepts/async-await), [Event Loop](/concepts/event-loop) + </Accordion> + + <Accordion title="Objects and Prototypes"> + - How does prototypal inheritance work? + - What's the difference between classical and prototypal inheritance? + - Explain `Object.create()`. + - What's the prototype chain? + + **Study:** [Object Creation and Prototypes](/concepts/object-creation-prototypes) + </Accordion> + + <Accordion title="Data Structures and Algorithms"> + - Implement common array methods (map, filter, reduce). + - What's the time complexity of [operation]? + - When would you use a Map vs an Object? + + **Study:** [Data Structures](/concepts/data-structures), [Algorithms and Big O](/concepts/algorithms-big-o) + </Accordion> +</AccordionGroup> + +<Tip> +**Practice explaining out loud.** In interviews, you need to articulate your thinking. Practice explaining each concept as if you're teaching someone else. +</Tip> + +--- + +## Topic-Based Paths + +Want to focus on a specific area? Here are paths organized by topic: + +### Async Mastery + +1. [Callbacks](/concepts/callbacks) +2. [Promises](/concepts/promises) +3. [async/await](/concepts/async-await) +4. [Event Loop](/concepts/event-loop) +5. [Generators and Iterators](/concepts/generators-iterators) + +### Object-Oriented JavaScript + +1. [Factories and Classes](/concepts/factories-classes) +2. [this, call, apply, bind](/concepts/this-call-apply-bind) +3. [Object Creation and Prototypes](/concepts/object-creation-prototypes) +4. [Inheritance and Polymorphism](/concepts/inheritance-polymorphism) + +### Functional Programming + +1. [Pure Functions](/concepts/pure-functions) +2. [Higher-Order Functions](/concepts/higher-order-functions) +3. [map, reduce, filter](/concepts/map-reduce-filter) +4. [Recursion](/concepts/recursion) +5. [Currying and Composition](/concepts/currying-composition) + +### Web Development + +1. [DOM](/concepts/dom) +2. [HTTP and Fetch](/concepts/http-fetch) +3. [Web Workers](/concepts/web-workers) +4. [ES Modules](/concepts/es-modules) + +--- + +## Start Learning + +<CardGroup cols={2}> + <Card title="Primitive Types" icon="play" href="/concepts/primitive-types"> + Begin with the first concept + </Card> + <Card title="All Concepts" icon="list" href="/getting-started/about"> + See the full list of 33 concepts + </Card> +</CardGroup> diff --git a/docs/getting-started/prerequisites.mdx b/docs/getting-started/prerequisites.mdx new file mode 100644 index 00000000..e0af518a --- /dev/null +++ b/docs/getting-started/prerequisites.mdx @@ -0,0 +1,170 @@ +--- +title: "JavaScript Setup: Tools You Need to Start Learning" +sidebarTitle: "Prerequisites" +description: "Set up your JavaScript learning environment in minutes. All you need is a browser and optionally a code editor. Perfect for complete beginners." +--- + +## What Do You Need to Learn JavaScript? + +This guide is designed for everyone, including complete beginners. You don't need to know any programming language before starting. + +All you need are a few free tools that you probably already have. + +--- + +## Required: A Web Browser + +JavaScript runs in every web browser. You can use any modern browser: + +| Browser | DevTools Shortcut | +|---------|------------------| +| **Chrome** (recommended) | F12 or Cmd+Option+J (Mac) | +| **Firefox** | F12 or Cmd+Option+I (Mac) | +| **Safari** | Cmd+Option+C (enable in Preferences first) | +| **Edge** | F12 | + +<Tip> +**We recommend Chrome** for learning. It has excellent developer tools and most tutorials use it for screenshots and examples. +</Tip> + +### Using the Browser Console + +The browser console is where you'll run JavaScript code. Here's how to open it: + +<Steps> + <Step title="Open any webpage"> + Even a blank tab works + </Step> + <Step title="Open Developer Tools"> + Press **F12** (Windows/Linux) or **Cmd+Option+J** (Mac) + </Step> + <Step title="Click the Console tab"> + This is your JavaScript playground + </Step> + <Step title="Type code and press Enter"> + Try typing `console.log("Hello!")` and press Enter + </Step> +</Steps> + +```javascript +// Type this in your console right now +console.log("Hello, JavaScript!") +// Output: Hello, JavaScript! + +// Try some math +2 + 2 +// Output: 4 + +// Create a variable +const name = "Your Name" +console.log(name) +// Output: Your Name +``` + +That's it. You're ready to learn JavaScript. + +--- + +## Recommended: A Code Editor + +While you can learn a lot in the browser console, a code editor makes writing longer code much easier. + +### Free Options + +<CardGroup cols={2}> + <Card title="VS Code" icon="code" href="https://code.visualstudio.com/"> + **Most popular choice.** Free, powerful, with excellent JavaScript support. Works on Windows, Mac, and Linux. + </Card> + <Card title="Sublime Text" icon="code" href="https://www.sublimetext.com/"> + **Fast and lightweight.** Free to evaluate, works on all platforms. + </Card> +</CardGroup> + +### Online Editors (No Installation) + +If you don't want to install anything, these online editors work great: + +<CardGroup cols={2}> + <Card title="CodePen" icon="codepen" href="https://codepen.io/"> + Great for quick experiments. See your code run instantly. + </Card> + <Card title="JSFiddle" icon="js" href="https://jsfiddle.net/"> + Simple and clean. Good for testing snippets. + </Card> + <Card title="StackBlitz" icon="bolt" href="https://stackblitz.com/"> + Full development environment in your browser. + </Card> + <Card title="CodeSandbox" icon="box" href="https://codesandbox.io/"> + Perfect for larger projects and frameworks. + </Card> +</CardGroup> + +--- + +## Optional: Node.js + +[Node.js](https://nodejs.org/) lets you run JavaScript outside the browser, on your computer's command line. + +**You don't need Node.js to learn from this guide.** Everything can be done in the browser. + +However, if you want to: +- Run JavaScript files from your terminal +- Use JavaScript for backend development later +- Follow along with some advanced tutorials + +Then install the **LTS (Long Term Support)** version from [nodejs.org](https://nodejs.org/). + +### Checking if Node.js is Installed + +Open your terminal (Command Prompt on Windows, Terminal on Mac/Linux) and type: + +```bash +node --version +``` + +If you see a version number like `v20.10.0`, you're good to go. + +--- + +## Your First JavaScript Code + +Let's make sure everything works. Open your browser console and type: + +```javascript +// Variables +const message = "I'm learning JavaScript!" +console.log(message) + +// A simple function +function greet(name) { + return "Hello, " + name + "!" +} + +console.log(greet("World")) +// Output: Hello, World! +``` + +If you see the output, congratulations! You're ready to start learning. + +--- + +## Summary + +| Tool | Required? | Purpose | +|------|-----------|---------| +| Web Browser | Yes | Run JavaScript, use DevTools console | +| Code Editor | Recommended | Write and save longer code | +| Node.js | Optional | Run JavaScript outside the browser | + +--- + +## Next Steps + +<CardGroup cols={2}> + <Card title="Learning Paths" icon="map" href="/getting-started/learning-paths"> + Find the right learning path for your goals + </Card> + <Card title="Start with Primitives" icon="play" href="/concepts/primitive-types"> + Jump into the first concept + </Card> +</CardGroup> diff --git a/docs/index.mdx b/docs/index.mdx new file mode 100644 index 00000000..03ddb476 --- /dev/null +++ b/docs/index.mdx @@ -0,0 +1,98 @@ +--- +title: "33 JavaScript Concepts Every Developer Should Know" +sidebarTitle: "Welcome" +description: "Learn the 33 essential JavaScript concepts every developer needs. Free guide with explanations, code examples, and curated resources for all skill levels." +--- + +<div className="not-prose"> + <div className="text-center py-12"> + <h1 className="text-5xl font-bold mb-4">33 JavaScript Concepts</h1> + <p className="text-xl text-gray-600 dark:text-gray-400 mb-8"> + Every JavaScript developer should know these fundamental concepts + </p> + <div className="flex justify-center gap-4 mb-8"> + <a href="/getting-started/about" className="px-6 py-3 bg-primary text-black font-semibold rounded-lg hover:opacity-90 transition"> + Get Started + </a> + <a href="https://github.com/leonardomso/33-js-concepts" className="px-6 py-3 border border-gray-300 dark:border-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition"> + View on GitHub + </a> + </div> + </div> +</div> + +--- + +JavaScript is the language of the web. Whether you're just starting out or have years of experience, understanding these 33 core concepts will make you a stronger developer. This guide breaks down each topic with clear explanations, practical code examples, and hand-picked resources to help you learn. + +--- + +## Why These 33 Concepts? + +This isn't just another tutorial. It's a roadmap to truly understanding how JavaScript works under the hood. Whether you're building websites, mobile apps, or servers, these concepts are the foundation. + +<CardGroup cols={2}> + <Card title="For Beginners" icon="seedling"> + No prior JavaScript knowledge required. Start from the fundamentals and build a solid foundation. + </Card> + <Card title="For Experienced Devs" icon="rocket"> + Fill in the gaps and deepen your understanding of concepts you use every day. + </Card> + <Card title="For Interview Prep" icon="briefcase"> + These concepts are commonly asked in technical interviews. Be ready to explain them. + </Card> + <Card title="For Everyone" icon="globe"> + Available in 40+ languages thanks to our community of contributors. + </Card> +</CardGroup> + +--- + +## What You'll Learn + +<CardGroup cols={2}> + <Card title="Fundamentals" icon="cube" href="/concepts/primitive-types"> + Types, Scope, Closures, Call Stack, and how JavaScript really works + </Card> + <Card title="Functions & Execution" icon="code" href="/concepts/event-loop"> + Event Loop, IIFE, Modules, and how code gets executed + </Card> + <Card title="Web Platform" icon="browser" href="/concepts/dom"> + DOM manipulation, HTTP requests with Fetch, and Web Workers + </Card> + <Card title="Object-Oriented JS" icon="sitemap" href="/concepts/factories-classes"> + Classes, Prototypes, `this` keyword, and inheritance + </Card> + <Card title="Async JavaScript" icon="clock" href="/concepts/promises"> + Callbacks, Promises, async/await, and handling asynchronous code + </Card> + <Card title="Functional Programming" icon="filter" href="/concepts/higher-order-functions"> + Pure functions, Higher-order functions, map/reduce/filter, and recursion + </Card> + <Card title="Advanced Topics" icon="graduation-cap" href="/concepts/data-structures"> + Data structures, Algorithms, Design patterns, and clean code + </Card> +</CardGroup> + +--- + +## A Community Effort + +<Tip> +**Recognized by GitHub** as one of the [top open source projects of 2018](https://github.blog/news-insights/octoverse/new-open-source-projects/#top-projects-of-2018)! +</Tip> + +This project was created by [Leonardo Maldonado](https://github.com/leonardomso) and has grown through contributions from hundreds of developers worldwide. It has been translated into over 40 languages, making JavaScript education accessible to everyone. + +--- + +## Ready to Begin? + +<CardGroup cols={2}> + <Card title="What is This Project?" icon="circle-info" href="/getting-started/about"> + Learn about the project's origin and what makes it different + </Card> + <Card title="Start Learning" icon="play" href="/concepts/primitive-types"> + Jump straight into the first concept + </Card> +</CardGroup> diff --git a/docs/translations.mdx b/docs/translations.mdx new file mode 100644 index 00000000..752cb353 --- /dev/null +++ b/docs/translations.mdx @@ -0,0 +1,94 @@ +--- +title: "Translations" +description: "33 JavaScript Concepts in 40+ languages" +--- + +## Community Translations + +Thanks to our amazing community, 33 JavaScript Concepts has been translated into over 40 languages! Feel free to submit a PR to add your own translation. + +<Info> +Want to contribute a translation? Check out our [Contributing Guide](/contributing) for instructions on how to create and submit a translation. +</Info> + +## Available Languages + +<CardGroup cols={2}> + <Card title="Arabic (اَلْعَرَبِيَّةُ‎)" icon="language" href="https://github.com/amrsekilly/33-js-concepts"> + By Amr Elsekilly + </Card> + <Card title="Bulgarian (Български)" icon="language" href="https://github.com/thewebmasterp/33-js-concepts"> + By thewebmasterp + </Card> + <Card title="Chinese (汉语)" icon="language" href="https://github.com/stephentian/33-js-concepts"> + By Re Tian + </Card> + <Card title="Brazilian Portuguese (Português do Brasil)" icon="language" href="https://github.com/tiagoboeing/33-js-concepts"> + By Tiago Boeing + </Card> + <Card title="Korean (한국어)" icon="language" href="https://github.com/yjs03057/33-js-concepts.git"> + By Suin Lee + </Card> + <Card title="Spanish (Español)" icon="language" href="https://github.com/adonismendozaperez/33-js-conceptos"> + By Adonis Mendoza + </Card> + <Card title="Turkish (Türkçe)" icon="language" href="https://github.com/ilker0/33-js-concepts"> + By İlker Demir + </Card> + <Card title="Russian (русский язык)" icon="language" href="https://github.com/gumennii/33-js-concepts"> + By Mihail Gumennii + </Card> +</CardGroup> + +## All Translations + +| Language | Translator | Repository | +|----------|------------|------------| +| Arabic (اَلْعَرَبِيَّةُ‎) | Amr Elsekilly | [Link](https://github.com/amrsekilly/33-js-concepts) | +| Bulgarian (Български) | thewebmasterp | [Link](https://github.com/thewebmasterp/33-js-concepts) | +| Chinese (汉语) | Re Tian | [Link](https://github.com/stephentian/33-js-concepts) | +| Brazilian Portuguese | Tiago Boeing | [Link](https://github.com/tiagoboeing/33-js-concepts) | +| Korean (한국어) | Suin Lee | [Link](https://github.com/yjs03057/33-js-concepts.git) | +| Spanish (Español) | Adonis Mendoza | [Link](https://github.com/adonismendozaperez/33-js-conceptos) | +| Turkish (Türkçe) | İlker Demir | [Link](https://github.com/ilker0/33-js-concepts) | +| Russian (русский язык) | Mihail Gumennii | [Link](https://github.com/gumennii/33-js-concepts) | +| Vietnamese (Tiếng Việt) | Nguyễn Trần Chung | [Link](https://github.com/nguyentranchung/33-js-concepts) | +| Polish (Polski) | Dawid Lipinski | [Link](https://github.com/lip3k/33-js-concepts) | +| Persian (فارسی) | Majid Alavizadeh | [Link](https://github.com/majidalavizadeh/33-js-concepts) | +| Indonesian (Bahasa Indonesia) | Rijdzuan Sampoerna | [Link](https://github.com/rijdz/33-js-concepts) | +| French (Français) | Robin Métral | [Link](https://github.com/robinmetral/33-concepts-js) | +| Hindi (हिन्दी) | Vikas Chauhan | [Link](https://github.com/vikaschauhan/33-js-concepts) | +| Greek (Ελληνικά) | Dimitris Zarachanis | [Link](https://github.com/DimitrisZx/33-js-concepts) | +| Japanese (日本語) | oimo23 | [Link](https://github.com/oimo23/33-js-concepts) | +| German (Deutsch) | burhannn | [Link](https://github.com/burhannn/33-js-concepts) | +| Ukrainian (украї́нська мо́ва) | Andrew Savetchuk | [Link](https://github.com/AndrewSavetchuk/33-js-concepts-ukrainian-translation) | +| Sinhala (සිංහල) | Udaya Shamendra | [Link](https://github.com/ududsha/33-js-concepts) | +| Italian (Italiano) | Gianluca Fiore | [Link](https://github.com/Donearm/33-js-concepts) | +| Latvian (Latviešu) | Jānis Īvāns | [Link](https://github.com/ANormalStick/33-js-concepts) | +| Oromo (Afaan Oromoo) | Amanuel Dagnachew | [Link](https://github.com/Amandagne/33-js-concepts) | +| Thai (ภาษาไทย) | Arif Waram | [Link](https://github.com/ninearif/33-js-concepts) | +| Catalan (Català) | Mario Estrada | [Link](https://github.com/marioestradaf/33-js-concepts) | +| Swedish (Svenska) | Fenix Hongell | [Link](https://github.com/FenixHongell/33-js-concepts/) | +| Khmer (ខ្មែរ) | Chrea Chanchhunneng | [Link](https://github.com/Chhunneng/33-js-concepts) | +| Ethiopian (አማርኛ) | Miniyahil Kebede | [Link](https://github.com/hmhard/33-js-concepts) | +| Belarussian (Беларуская мова) | Dzianis Yafimau | [Link](https://github.com/Yafimau/33-js-concepts) | +| Uzbek (O'zbekcha) | Shokhrukh Usmonov | [Link](https://github.com/smnv-shokh/33-js-concepts) | +| Urdu (اردو) | Yasir Nawaz | [Link](https://github.com/sudoyasir/33-js-concepts) | +| Bengali (বাংলা) | Jisan Mia | [Link](https://github.com/Jisan-mia/33-js-concepts) | +| Gujarati (ગુજરાતી) | Vatsal Bhuva | [Link](https://github.com/VatsalBhuva11/33-js-concepts) | +| Sindhi (سنڌي) | Sunny Gandhwani | [Link](https://github.com/Sunny-unik/33-js-concepts) | +| Bhojpuri (भोजपुरी) | Pronay Debnath | [Link](https://github.com/debnath003/33-js-concepts) | +| Punjabi (ਪੰਜਾਬੀ) | Harsh Dev Pathak | [Link](https://github.com/Harshdev098/33-js-concepts) | +| Malayalam (മലയാളം) | Akshay Manoj | [Link](https://github.com/Stark-Akshay/33-js-concepts) | +| Yoruba (Yorùbá) | Ayomide Bajulaye | [Link](https://github.com/ayobaj/33-js-concepts) | +| Hebrew (עברית‎) | Refael Yzgea | [Link](https://github.com/rafyzg/33-js-concepts) | +| Dutch (Nederlands) | Dave Visser | [Link](https://github.com/dlvisser/33-js-concepts) | +| Tamil (தமிழ்) | Udaya Krishnan M | [Link](https://github.com/UdayaKrishnanM/33-js-concepts) | + +## Create Your Own Translation + +Want to help make JavaScript concepts accessible in your language? + +<Card title="Start Translating" icon="globe" href="/contributing"> + Follow our contribution guidelines to create a translation +</Card> diff --git a/opencode.json b/opencode.json new file mode 100644 index 00000000..9222cb5f --- /dev/null +++ b/opencode.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://opencode.ai/config.json", + "mcp": { + "context7": { + "type": "remote", + "url": "https://mcp.context7.com/mcp" + }, + "github": { + "type": "local", + "command": ["bunx", "@modelcontextprotocol/server-github"], + "environment": { + "GITHUB_PERSONAL_ACCESS_TOKEN": "{env:GITHUB_PERSONAL_ACCESS_TOKEN}", + "GITHUB_TOOLSETS": "repos,issues,pull_requests,actions,code_security" + } + } + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..9f2e8242 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2273 @@ +{ + "name": "33-js-concepts", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "33-js-concepts", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "@vitest/coverage-v8": "^4.0.16", + "jsdom": "^27.4.0", + "vitest": "^4.0.16" + } + }, + "node_modules/@acemir/cssom": { + "version": "0.9.30", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.30.tgz", + "integrity": "sha512-9CnlMCI0LmCIq0olalQqdWrJHPzm0/tw3gzOA9zJSgvFX7Xau3D24mAGa4BtwxwY69nsuJW6kQqqCzf/mEcQgg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.1.tgz", + "integrity": "sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "lru-cache": "^11.2.4" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.7.6", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.6.tgz", + "integrity": "sha512-hBaJER6A9MpdG3WgdlOolHmbOYvSk46y7IQN/1+iqiCuUu6iWdQrs9DGKF8ocqsEqWujWf/V7b7vaDgiUmIvUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.4" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.0.22", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.22.tgz", + "integrity": "sha512-qBcx6zYlhleiFfdtzkRgwNC7VVoAwfK76Vmsw5t+PbvtdknO9StgRk7ROvq9so1iqbdW4uLIDAsXRsTfUrIoOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.7.0.tgz", + "integrity": "sha512-5i+BtvujK/vM07YCGDyz4C4AyDzLmhxHMtM5HpUyPRtJPBdFPsj290ffXW+UXY21/G7GtXeHD2nRmq0T1ShyQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@exodus/crypto": "^1.0.0-rc.4" + }, + "peerDependenciesMeta": { + "@exodus/crypto": { + "optional": true + } + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", + "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", + "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", + "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", + "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", + "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", + "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", + "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", + "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", + "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", + "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", + "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", + "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", + "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", + "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", + "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", + "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", + "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", + "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", + "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", + "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", + "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", + "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/coverage-v8": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.16.tgz", + "integrity": "sha512-2rNdjEIsPRzsdu6/9Eq0AYAzYdpP6Bx9cje9tL3FE5XzXRQF1fNU9pe/1yE8fCrS0HD+fBtt6gLPh6LI57tX7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.0.16", + "ast-v8-to-istanbul": "^0.3.8", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.1", + "obug": "^2.1.1", + "std-env": "^3.10.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.0.16", + "vitest": "4.0.16" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.16.tgz", + "integrity": "sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.16", + "@vitest/utils": "4.0.16", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.16.tgz", + "integrity": "sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.16", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.16.tgz", + "integrity": "sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.16.tgz", + "integrity": "sha512-VWEDm5Wv9xEo80ctjORcTQRJ539EGPB3Pb9ApvVRAY1U/WkHXmmYISqU5E79uCwcW7xYUV38gwZD+RV755fu3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.16", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.16.tgz", + "integrity": "sha512-sf6NcrYhYBsSYefxnry+DR8n3UV4xWZwWxYbCJUt2YdvtqzSPR7VfGrY0zsv090DAbjFZsi7ZaMi1KnSRyK1XA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.16", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.16.tgz", + "integrity": "sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.16.tgz", + "integrity": "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.16", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.10.tgz", + "integrity": "sha512-p4K7vMz2ZSk3wN8l5o3y2bJAoZXT3VuJI5OLTATY/01CYWumWvwkUw0SqDBnNq6IiTO3qDa1eSQDibAV8g7XOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^9.0.1" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/cssstyle": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.5.tgz", + "integrity": "sha512-GlsEptulso7Jg0VaOZ8BXQi3AkYM5BOJKEO/rjMidSCq70FkIC5y0eawrCXeYzxgt3OCf4Ls+eoxN+/05vN0Ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^4.1.1", + "@csstools/css-syntax-patches-for-csstree": "^1.0.21", + "css-tree": "^3.1.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz", + "integrity": "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "27.4.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.4.0.tgz", + "integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@acemir/cssom": "^0.9.28", + "@asamuzakjp/dom-selector": "^6.7.6", + "@exodus/bytes": "^1.6.0", + "cssstyle": "^5.3.4", + "data-urls": "^6.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.0", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.1.0", + "ws": "^8.18.3", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/lru-cache": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.1.tgz", + "integrity": "sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", + "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.54.0", + "@rollup/rollup-android-arm64": "4.54.0", + "@rollup/rollup-darwin-arm64": "4.54.0", + "@rollup/rollup-darwin-x64": "4.54.0", + "@rollup/rollup-freebsd-arm64": "4.54.0", + "@rollup/rollup-freebsd-x64": "4.54.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", + "@rollup/rollup-linux-arm-musleabihf": "4.54.0", + "@rollup/rollup-linux-arm64-gnu": "4.54.0", + "@rollup/rollup-linux-arm64-musl": "4.54.0", + "@rollup/rollup-linux-loong64-gnu": "4.54.0", + "@rollup/rollup-linux-ppc64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-musl": "4.54.0", + "@rollup/rollup-linux-s390x-gnu": "4.54.0", + "@rollup/rollup-linux-x64-gnu": "4.54.0", + "@rollup/rollup-linux-x64-musl": "4.54.0", + "@rollup/rollup-openharmony-arm64": "4.54.0", + "@rollup/rollup-win32-arm64-msvc": "4.54.0", + "@rollup/rollup-win32-ia32-msvc": "4.54.0", + "@rollup/rollup-win32-x64-gnu": "4.54.0", + "@rollup/rollup-win32-x64-msvc": "4.54.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.0.19", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.19.tgz", + "integrity": "sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.19" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.19", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.19.tgz", + "integrity": "sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/vite": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", + "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.16.tgz", + "integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.16", + "@vitest/mocker": "4.0.16", + "@vitest/pretty-format": "4.0.16", + "@vitest/runner": "4.0.16", + "@vitest/snapshot": "4.0.16", + "@vitest/spy": "4.0.16", + "@vitest/utils": "4.0.16", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.16", + "@vitest/browser-preview": "4.0.16", + "@vitest/browser-webdriverio": "4.0.16", + "@vitest/ui": "4.0.16", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz", + "integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/package.json b/package.json index 588446b6..350ba6c8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "33-js-concepts", "version": "1.0.0", - "description": "33 concepts every JavaScript developer should know.", + "description": "A curated collection of 33 essential JavaScript concepts every developer should master. Includes comprehensive learning resources, articles, videos, and interactive code examples covering everything from call stack and closures to async/await and design patterns.", "main": "index.js", "author": { "name": "Leonardo Maldonado", @@ -9,28 +9,53 @@ }, "license": "MIT", "bugs": { - "url": "https://github.com/leonardomso/33/issues" + "url": "https://github.com/leonardomso/33-js-concepts/issues" }, - "homepage": "https://github.com/leonardomso/33#readme", + "homepage": "https://github.com/leonardomso/33-js-concepts#readme", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", + "docs": "cd docs && npx mintlify dev", + "docs:build": "cd docs && npx mintlify build" }, "repository": { "type": "git", - "url": "git+https://github.com/leonardomso/33.git" + "url": "git+https://github.com/leonardomso/33-js-concepts.git" }, "keywords": [ - "JavaScript", "javascript", - "JS", - "programming", - "web", - "web dev", - "front end", - "front-end", + "javascript-concepts", + "javascript-learning", + "javascript-tutorial", + "learn-javascript", + "closures", + "promises", + "async-await", + "event-loop", + "prototypes", + "scope", + "hoisting", + "coercion", + "this-keyword", + "call-stack", + "higher-order-functions", + "functional-programming", + "design-patterns", + "data-structures", + "algorithms", + "es6", + "ecmascript", + "web-development", + "frontend", "nodejs", - "Node.js", - "NodeJS", - "Node" - ] + "programming", + "developer-resources", + "interview-preparation" + ], + "devDependencies": { + "@vitest/coverage-v8": "^4.0.16", + "jsdom": "^27.4.0", + "vitest": "^4.0.16" + } } diff --git a/tests/advanced-topics/algorithms-big-o/algorithms-big-o.test.js b/tests/advanced-topics/algorithms-big-o/algorithms-big-o.test.js new file mode 100644 index 00000000..94a934de --- /dev/null +++ b/tests/advanced-topics/algorithms-big-o/algorithms-big-o.test.js @@ -0,0 +1,523 @@ +import { describe, it, expect } from 'vitest' + +// ============================================ +// SEARCHING ALGORITHMS +// ============================================ + +// Linear Search - O(n) +function linearSearch(arr, target) { + for (let i = 0; i < arr.length; i++) { + if (arr[i] === target) return i + } + return -1 +} + +// Binary Search - O(log n) +function binarySearch(arr, target) { + let left = 0 + let right = arr.length - 1 + + while (left <= right) { + const mid = Math.floor((left + right) / 2) + + if (arr[mid] === target) return mid + if (arr[mid] < target) left = mid + 1 + else right = mid - 1 + } + + return -1 +} + +// ============================================ +// SORTING ALGORITHMS +// ============================================ + +// Bubble Sort - O(n²) average/worst, O(n) best with early termination +function bubbleSort(arr) { + const result = [...arr] + const n = result.length + + for (let i = 0; i < n; i++) { + let swapped = false + + for (let j = 0; j < n - i - 1; j++) { + if (result[j] > result[j + 1]) { + [result[j], result[j + 1]] = [result[j + 1], result[j]] + swapped = true + } + } + + // If no swaps occurred, array is sorted + if (!swapped) break + } + + return result +} + +// Merge Sort - O(n log n) +function mergeSort(arr) { + if (arr.length <= 1) return arr + + const mid = Math.floor(arr.length / 2) + const left = mergeSort(arr.slice(0, mid)) + const right = mergeSort(arr.slice(mid)) + + return merge(left, right) +} + +function merge(left, right) { + const result = [] + let i = 0 + let j = 0 + + while (i < left.length && j < right.length) { + if (left[i] <= right[j]) { + result.push(left[i]) + i++ + } else { + result.push(right[j]) + j++ + } + } + + return result.concat(left.slice(i)).concat(right.slice(j)) +} + +// ============================================ +// INTERVIEW PATTERNS +// ============================================ + +// Two Pointers - Find pair that sums to target +function twoSum(arr, target) { + let left = 0 + let right = arr.length - 1 + + while (left < right) { + const sum = arr[left] + arr[right] + + if (sum === target) return [left, right] + if (sum < target) left++ + else right-- + } + + return null +} + +// Sliding Window - Maximum sum of k consecutive elements +function maxSumSubarray(arr, k) { + if (arr.length < k) return null + + let windowSum = 0 + for (let i = 0; i < k; i++) { + windowSum += arr[i] + } + + let maxSum = windowSum + + for (let i = k; i < arr.length; i++) { + windowSum = windowSum - arr[i - k] + arr[i] + maxSum = Math.max(maxSum, windowSum) + } + + return maxSum +} + +// Frequency Counter - Check anagrams +function isAnagram(str1, str2) { + if (str1.length !== str2.length) return false + + const freq = {} + + for (const char of str1) { + freq[char] = (freq[char] || 0) + 1 + } + + for (const char of str2) { + if (!freq[char]) return false + freq[char]-- + } + + return true +} + +// Has Duplicates - O(n) with Set +function hasDuplicates(arr) { + const seen = new Set() + for (const item of arr) { + if (seen.has(item)) return true + seen.add(item) + } + return false +} + +// Longest Unique Substring - Sliding Window +function longestUniqueSubstring(s) { + const seen = new Set() + let maxLen = 0 + let left = 0 + + for (let right = 0; right < s.length; right++) { + while (seen.has(s[right])) { + seen.delete(s[left]) + left++ + } + seen.add(s[right]) + maxLen = Math.max(maxLen, right - left + 1) + } + + return maxLen +} + +// ============================================ +// TESTS +// ============================================ + +describe('Algorithms & Big O', () => { + describe('Searching Algorithms', () => { + describe('Linear Search', () => { + it('should find element at beginning', () => { + expect(linearSearch([1, 2, 3, 4, 5], 1)).toBe(0) + }) + + it('should find element at end', () => { + expect(linearSearch([1, 2, 3, 4, 5], 5)).toBe(4) + }) + + it('should find element in middle', () => { + expect(linearSearch([3, 7, 1, 9, 4], 9)).toBe(3) + }) + + it('should return -1 when element not found', () => { + expect(linearSearch([1, 2, 3, 4, 5], 10)).toBe(-1) + }) + + it('should handle empty array', () => { + expect(linearSearch([], 1)).toBe(-1) + }) + + it('should find first occurrence of duplicates', () => { + expect(linearSearch([1, 2, 3, 2, 5], 2)).toBe(1) + }) + }) + + describe('Binary Search', () => { + it('should find element in sorted array', () => { + expect(binarySearch([1, 3, 5, 7, 9, 11, 13], 9)).toBe(4) + }) + + it('should find first element', () => { + expect(binarySearch([1, 3, 5, 7, 9], 1)).toBe(0) + }) + + it('should find last element', () => { + expect(binarySearch([1, 3, 5, 7, 9], 9)).toBe(4) + }) + + it('should return -1 when element not found', () => { + expect(binarySearch([1, 3, 5, 7, 9], 6)).toBe(-1) + }) + + it('should handle single element array - found', () => { + expect(binarySearch([5], 5)).toBe(0) + }) + + it('should handle single element array - not found', () => { + expect(binarySearch([5], 3)).toBe(-1) + }) + + it('should handle empty array', () => { + expect(binarySearch([], 5)).toBe(-1) + }) + + it('should work with large sorted array', () => { + const arr = Array.from({ length: 1000 }, (_, i) => i * 2) // [0, 2, 4, ..., 1998] + expect(binarySearch(arr, 500)).toBe(250) + expect(binarySearch(arr, 501)).toBe(-1) + }) + }) + }) + + describe('Sorting Algorithms', () => { + describe('Bubble Sort', () => { + it('should sort array in ascending order', () => { + expect(bubbleSort([5, 3, 8, 4, 2])).toEqual([2, 3, 4, 5, 8]) + }) + + it('should handle already sorted array', () => { + expect(bubbleSort([1, 2, 3, 4, 5])).toEqual([1, 2, 3, 4, 5]) + }) + + it('should handle reverse sorted array', () => { + expect(bubbleSort([5, 4, 3, 2, 1])).toEqual([1, 2, 3, 4, 5]) + }) + + it('should handle array with duplicates', () => { + expect(bubbleSort([3, 1, 4, 1, 5, 9, 2, 6])).toEqual([1, 1, 2, 3, 4, 5, 6, 9]) + }) + + it('should handle single element', () => { + expect(bubbleSort([42])).toEqual([42]) + }) + + it('should handle empty array', () => { + expect(bubbleSort([])).toEqual([]) + }) + + it('should not mutate original array', () => { + const original = [3, 1, 4, 1, 5] + bubbleSort(original) + expect(original).toEqual([3, 1, 4, 1, 5]) + }) + + it('should handle negative numbers', () => { + expect(bubbleSort([-3, -1, -4, -1, -5])).toEqual([-5, -4, -3, -1, -1]) + }) + + it('should terminate early on already sorted array (O(n) best case)', () => { + // This test verifies the early termination optimization works + // On an already sorted array, only one pass is needed + const sorted = [1, 2, 3, 4, 5] + expect(bubbleSort(sorted)).toEqual([1, 2, 3, 4, 5]) + }) + }) + + describe('Merge Sort', () => { + it('should sort array in ascending order', () => { + expect(mergeSort([38, 27, 43, 3, 9, 82, 10])).toEqual([3, 9, 10, 27, 38, 43, 82]) + }) + + it('should handle already sorted array', () => { + expect(mergeSort([1, 2, 3, 4, 5])).toEqual([1, 2, 3, 4, 5]) + }) + + it('should handle reverse sorted array', () => { + expect(mergeSort([5, 4, 3, 2, 1])).toEqual([1, 2, 3, 4, 5]) + }) + + it('should handle array with duplicates', () => { + expect(mergeSort([3, 1, 4, 1, 5, 9, 2, 6])).toEqual([1, 1, 2, 3, 4, 5, 6, 9]) + }) + + it('should handle single element', () => { + expect(mergeSort([42])).toEqual([42]) + }) + + it('should handle empty array', () => { + expect(mergeSort([])).toEqual([]) + }) + + it('should handle negative numbers', () => { + expect(mergeSort([-3, 1, -4, 1, -5, 9])).toEqual([-5, -4, -3, 1, 1, 9]) + }) + + it('should maintain stability for equal elements', () => { + // Merge sort is stable - equal elements maintain relative order + const result = mergeSort([3, 1, 2, 1]) + expect(result).toEqual([1, 1, 2, 3]) + }) + }) + }) + + describe('Interview Patterns', () => { + describe('Two Pointers - Two Sum', () => { + it('should find pair that sums to target', () => { + expect(twoSum([1, 3, 5, 7, 9], 12)).toEqual([1, 4]) // 3 + 9 = 12 + }) + + it('should find pair at extremes', () => { + expect(twoSum([1, 2, 3, 4, 5], 6)).toEqual([0, 4]) // 1 + 5 = 6 + }) + + it('should find adjacent pair', () => { + expect(twoSum([1, 2, 3, 4, 5], 9)).toEqual([3, 4]) // 4 + 5 = 9 + }) + + it('should return null when no pair exists', () => { + expect(twoSum([1, 2, 3, 4, 5], 100)).toBe(null) + }) + + it('should handle minimum size array', () => { + expect(twoSum([3, 7], 10)).toEqual([0, 1]) + }) + + it('should return null for single element', () => { + expect(twoSum([5], 10)).toBe(null) + }) + }) + + describe('Sliding Window - Max Sum Subarray', () => { + it('should find maximum sum of k consecutive elements', () => { + expect(maxSumSubarray([2, 1, 5, 1, 3, 2], 3)).toBe(9) // 5 + 1 + 3 + }) + + it('should handle window at beginning', () => { + expect(maxSumSubarray([9, 1, 1, 1, 1, 1], 2)).toBe(10) // 9 + 1 + }) + + it('should handle window at end', () => { + expect(maxSumSubarray([1, 1, 1, 1, 9, 8], 2)).toBe(17) // 9 + 8 + }) + + it('should handle k equal to array length', () => { + expect(maxSumSubarray([1, 2, 3], 3)).toBe(6) + }) + + it('should return null if array shorter than k', () => { + expect(maxSumSubarray([1, 2], 3)).toBe(null) + }) + + it('should handle negative numbers', () => { + expect(maxSumSubarray([-1, -2, 5, 6, -3], 2)).toBe(11) // 5 + 6 + }) + + it('should handle all negative numbers', () => { + expect(maxSumSubarray([-5, -3, -8, -2], 2)).toBe(-8) // -5 + -3 = -8 is max + }) + }) + + describe('Frequency Counter - Anagram Check', () => { + it('should return true for valid anagrams', () => { + expect(isAnagram('listen', 'silent')).toBe(true) + }) + + it('should return true for same string', () => { + expect(isAnagram('hello', 'hello')).toBe(true) + }) + + it('should return false for different lengths', () => { + expect(isAnagram('hello', 'helloo')).toBe(false) + }) + + it('should return false for non-anagrams', () => { + expect(isAnagram('hello', 'world')).toBe(false) + }) + + it('should handle empty strings', () => { + expect(isAnagram('', '')).toBe(true) + }) + + it('should be case sensitive', () => { + expect(isAnagram('Listen', 'Silent')).toBe(false) + }) + + it('should handle repeated characters', () => { + expect(isAnagram('aab', 'baa')).toBe(true) + expect(isAnagram('aab', 'bba')).toBe(false) + }) + + it('should handle single characters', () => { + expect(isAnagram('a', 'a')).toBe(true) + expect(isAnagram('a', 'b')).toBe(false) + }) + }) + + describe('Has Duplicates', () => { + it('should return true when duplicates exist', () => { + expect(hasDuplicates([1, 2, 3, 2, 5])).toBe(true) + }) + + it('should return false when no duplicates', () => { + expect(hasDuplicates([1, 2, 3, 4, 5])).toBe(false) + }) + + it('should handle empty array', () => { + expect(hasDuplicates([])).toBe(false) + }) + + it('should handle single element', () => { + expect(hasDuplicates([1])).toBe(false) + }) + + it('should detect duplicates at beginning', () => { + expect(hasDuplicates([1, 1, 2, 3, 4])).toBe(true) + }) + + it('should detect duplicates at end', () => { + expect(hasDuplicates([1, 2, 3, 4, 4])).toBe(true) + }) + + it('should work with strings', () => { + expect(hasDuplicates(['a', 'b', 'c', 'a'])).toBe(true) + expect(hasDuplicates(['a', 'b', 'c', 'd'])).toBe(false) + }) + }) + + describe('Longest Unique Substring', () => { + it('should find longest substring without repeating characters', () => { + expect(longestUniqueSubstring('abcabcbb')).toBe(3) // "abc" + }) + + it('should handle all same characters', () => { + expect(longestUniqueSubstring('bbbbb')).toBe(1) + }) + + it('should handle unique characters at end', () => { + expect(longestUniqueSubstring('pwwkew')).toBe(3) // "wke" + }) + + it('should handle empty string', () => { + expect(longestUniqueSubstring('')).toBe(0) + }) + + it('should handle single character', () => { + expect(longestUniqueSubstring('a')).toBe(1) + }) + + it('should handle all unique characters', () => { + expect(longestUniqueSubstring('abcdef')).toBe(6) + }) + + it('should handle repeating pattern', () => { + expect(longestUniqueSubstring('abab')).toBe(2) + }) + }) + }) + + describe('Big O Concepts', () => { + describe('Array operations complexity', () => { + it('should demonstrate O(1) array access', () => { + const arr = [1, 2, 3, 4, 5] + // Direct index access is O(1) + expect(arr[0]).toBe(1) + expect(arr[4]).toBe(5) + expect(arr[2]).toBe(3) + }) + + it('should demonstrate O(1) push and pop', () => { + const arr = [1, 2, 3] + arr.push(4) // O(1) + expect(arr).toEqual([1, 2, 3, 4]) + const popped = arr.pop() // O(1) + expect(popped).toBe(4) + expect(arr).toEqual([1, 2, 3]) + }) + + it('should demonstrate O(n) shift and unshift', () => { + const arr = [1, 2, 3] + // These are O(n) because they require re-indexing all elements + arr.unshift(0) + expect(arr).toEqual([0, 1, 2, 3]) + const shifted = arr.shift() + expect(shifted).toBe(0) + expect(arr).toEqual([1, 2, 3]) + }) + }) + + describe('Set vs Array for lookups', () => { + it('should demonstrate Set.has() is faster than Array.includes() for repeated lookups', () => { + const arr = Array.from({ length: 1000 }, (_, i) => i) + const set = new Set(arr) + + // Both find the element, but Set.has() is O(1) vs Array.includes() O(n) + expect(arr.includes(500)).toBe(true) + expect(set.has(500)).toBe(true) + + expect(arr.includes(999)).toBe(true) + expect(set.has(999)).toBe(true) + + expect(arr.includes(1001)).toBe(false) + expect(set.has(1001)).toBe(false) + }) + }) + }) +}) diff --git a/tests/advanced-topics/data-structures/data-structures.test.js b/tests/advanced-topics/data-structures/data-structures.test.js new file mode 100644 index 00000000..bfa89fee --- /dev/null +++ b/tests/advanced-topics/data-structures/data-structures.test.js @@ -0,0 +1,1054 @@ +import { describe, it, expect } from 'vitest' + +describe('Data Structures', () => { + describe('Arrays', () => { + it('should access elements by index in O(1)', () => { + const arr = ['a', 'b', 'c', 'd', 'e'] + + expect(arr[0]).toBe('a') + expect(arr[2]).toBe('c') + expect(arr[4]).toBe('e') + }) + + it('should add and remove from end with push/pop in O(1)', () => { + const arr = [1, 2, 3] + + arr.push(4) + expect(arr).toEqual([1, 2, 3, 4]) + + const popped = arr.pop() + expect(popped).toBe(4) + expect(arr).toEqual([1, 2, 3]) + }) + + it('should add and remove from beginning with unshift/shift (O(n))', () => { + const arr = [1, 2, 3] + + arr.unshift(0) + expect(arr).toEqual([0, 1, 2, 3]) + + const shifted = arr.shift() + expect(shifted).toBe(0) + expect(arr).toEqual([1, 2, 3]) + }) + + it('should search with indexOf and includes in O(n)', () => { + const arr = ['apple', 'banana', 'cherry'] + + expect(arr.indexOf('banana')).toBe(1) + expect(arr.indexOf('mango')).toBe(-1) + expect(arr.includes('cherry')).toBe(true) + expect(arr.includes('grape')).toBe(false) + }) + + it('should insert in middle with splice in O(n)', () => { + const arr = [1, 2, 4, 5] + + // Insert 3 at index 2 + arr.splice(2, 0, 3) + expect(arr).toEqual([1, 2, 3, 4, 5]) + + // Remove element at index 2 + arr.splice(2, 1) + expect(arr).toEqual([1, 2, 4, 5]) + }) + }) + + describe('Objects', () => { + it('should access, add, and delete properties in O(1)', () => { + const user = { name: 'Alice', age: 30 } + + // Access + expect(user.name).toBe('Alice') + expect(user['age']).toBe(30) + + // Add + user.email = 'alice@example.com' + expect(user.email).toBe('alice@example.com') + + // Delete + delete user.email + expect(user.email).toBe(undefined) + }) + + it('should check for key existence', () => { + const user = { name: 'Alice' } + + expect('name' in user).toBe(true) + expect('age' in user).toBe(false) + expect(user.hasOwnProperty('name')).toBe(true) + }) + + it('should convert numeric keys to strings', () => { + const obj = {} + obj[1] = 'one' + obj['1'] = 'one as string' + + // Both are the same key! + expect(Object.keys(obj)).toEqual(['1']) + expect(obj[1]).toBe('one as string') + expect(obj['1']).toBe('one as string') + }) + }) + + describe('Map', () => { + it('should use any value type as key', () => { + const map = new Map() + + const objKey = { id: 1 } + const funcKey = () => {} + + map.set('string', 'string key') + map.set(123, 'number key') + map.set(objKey, 'object key') + map.set(funcKey, 'function key') + map.set(true, 'boolean key') + + expect(map.get('string')).toBe('string key') + expect(map.get(123)).toBe('number key') + expect(map.get(objKey)).toBe('object key') + expect(map.get(funcKey)).toBe('function key') + expect(map.get(true)).toBe('boolean key') + }) + + it('should have a size property', () => { + const map = new Map() + map.set('a', 1) + map.set('b', 2) + map.set('c', 3) + + expect(map.size).toBe(3) + }) + + it('should check existence with has()', () => { + const map = new Map([['key', 'value']]) + + expect(map.has('key')).toBe(true) + expect(map.has('nonexistent')).toBe(false) + }) + + it('should delete entries', () => { + const map = new Map([['a', 1], ['b', 2]]) + + map.delete('a') + expect(map.has('a')).toBe(false) + expect(map.size).toBe(1) + }) + + it('should maintain insertion order', () => { + const map = new Map() + map.set('first', 1) + map.set('second', 2) + map.set('third', 3) + + const keys = [...map.keys()] + expect(keys).toEqual(['first', 'second', 'third']) + }) + + it('should iterate with for...of', () => { + const map = new Map([['a', 1], ['b', 2]]) + const entries = [] + + for (const [key, value] of map) { + entries.push([key, value]) + } + + expect(entries).toEqual([['a', 1], ['b', 2]]) + }) + + it('should be useful for counting occurrences', () => { + function countWords(text) { + const words = text.toLowerCase().split(/\s+/) + const counts = new Map() + + for (const word of words) { + counts.set(word, (counts.get(word) || 0) + 1) + } + + return counts + } + + const result = countWords('the cat and the dog') + expect(result.get('the')).toBe(2) + expect(result.get('cat')).toBe(1) + expect(result.get('and')).toBe(1) + }) + }) + + describe('Set', () => { + it('should store only unique values', () => { + const set = new Set() + + set.add(1) + set.add(2) + set.add(2) // Duplicate - ignored + set.add(3) + set.add(3) // Duplicate - ignored + + expect(set.size).toBe(3) + expect([...set]).toEqual([1, 2, 3]) + }) + + it('should check existence with has()', () => { + const set = new Set([1, 2, 3]) + + expect(set.has(2)).toBe(true) + expect(set.has(5)).toBe(false) + }) + + it('should remove duplicates from array', () => { + const numbers = [1, 2, 2, 3, 3, 3, 4] + const unique = [...new Set(numbers)] + + expect(unique).toEqual([1, 2, 3, 4]) + }) + + it('should delete values', () => { + const set = new Set([1, 2, 3]) + + set.delete(2) + expect(set.has(2)).toBe(false) + expect(set.size).toBe(2) + }) + + it('should iterate in insertion order', () => { + const set = new Set() + set.add('first') + set.add('second') + set.add('third') + + expect([...set]).toEqual(['first', 'second', 'third']) + }) + + it('should perform set operations (ES2024+)', () => { + const a = new Set([1, 2, 3]) + const b = new Set([2, 3, 4]) + + // Skip if ES2024 Set methods not available + if (typeof a.union !== 'function') { + // Manual implementation for older environments + const union = new Set([...a, ...b]) + expect([...union].sort()).toEqual([1, 2, 3, 4]) + + const intersection = new Set([...a].filter(x => b.has(x))) + expect([...intersection].sort()).toEqual([2, 3]) + + const difference = new Set([...a].filter(x => !b.has(x))) + expect([...difference]).toEqual([1]) + + return + } + + // Union: elements in either set + expect([...a.union(b)].sort()).toEqual([1, 2, 3, 4]) + + // Intersection: elements in both sets + expect([...a.intersection(b)].sort()).toEqual([2, 3]) + + // Difference: elements in a but not in b + expect([...a.difference(b)]).toEqual([1]) + + // Symmetric difference: elements in either but not both + expect([...a.symmetricDifference(b)].sort()).toEqual([1, 4]) + }) + + it('should check subset relationships (ES2024+)', () => { + const small = new Set([1, 2]) + const large = new Set([1, 2, 3, 4]) + + // Skip if ES2024 Set methods not available + if (typeof small.isSubsetOf !== 'function') { + // Manual implementation for older environments + const isSubset = [...small].every(x => large.has(x)) + expect(isSubset).toBe(true) + + const isSuperset = [...small].every(x => large.has(x)) + expect(isSuperset).toBe(true) + + const largeIsSubsetOfSmall = [...large].every(x => small.has(x)) + expect(largeIsSubsetOfSmall).toBe(false) + + return + } + + expect(small.isSubsetOf(large)).toBe(true) + expect(large.isSupersetOf(small)).toBe(true) + expect(large.isSubsetOf(small)).toBe(false) + }) + }) + + describe('WeakMap', () => { + it('should only accept objects as keys', () => { + const weakMap = new WeakMap() + const obj = { id: 1 } + + weakMap.set(obj, 'value') + expect(weakMap.get(obj)).toBe('value') + + // Cannot use primitives as keys + expect(() => weakMap.set('string', 'value')).toThrow(TypeError) + }) + + it('should support get, set, has, delete operations', () => { + const weakMap = new WeakMap() + const obj = { id: 1 } + + weakMap.set(obj, 'data') + expect(weakMap.has(obj)).toBe(true) + expect(weakMap.get(obj)).toBe('data') + + weakMap.delete(obj) + expect(weakMap.has(obj)).toBe(false) + }) + + it('should be useful for private data pattern', () => { + const privateData = new WeakMap() + + class User { + constructor(name, password) { + this.name = name + privateData.set(this, { password }) + } + + checkPassword(input) { + return privateData.get(this).password === input + } + } + + const user = new User('Alice', 'secret123') + expect(user.name).toBe('Alice') + expect(user.password).toBe(undefined) // Not accessible + expect(user.checkPassword('secret123')).toBe(true) + expect(user.checkPassword('wrong')).toBe(false) + }) + }) + + describe('WeakSet', () => { + it('should only accept objects as values', () => { + const weakSet = new WeakSet() + const obj = { id: 1 } + + weakSet.add(obj) + expect(weakSet.has(obj)).toBe(true) + + // Cannot use primitives + expect(() => weakSet.add('string')).toThrow(TypeError) + }) + + it('should be useful for tracking processed objects', () => { + const processed = new WeakSet() + + function processOnce(obj) { + if (processed.has(obj)) { + return 'already processed' + } + processed.add(obj) + return 'processed' + } + + const obj = { data: 'test' } + expect(processOnce(obj)).toBe('processed') + expect(processOnce(obj)).toBe('already processed') + }) + }) + + describe('Stack Implementation', () => { + class Stack { + constructor() { + this.items = [] + } + + push(item) { + this.items.push(item) + } + + pop() { + return this.items.pop() + } + + peek() { + return this.items[this.items.length - 1] + } + + isEmpty() { + return this.items.length === 0 + } + + size() { + return this.items.length + } + } + + it('should follow LIFO (Last In, First Out)', () => { + const stack = new Stack() + + stack.push(1) + stack.push(2) + stack.push(3) + + expect(stack.pop()).toBe(3) // Last in + expect(stack.pop()).toBe(2) + expect(stack.pop()).toBe(1) // First in + }) + + it('should peek without removing', () => { + const stack = new Stack() + stack.push('a') + stack.push('b') + + expect(stack.peek()).toBe('b') + expect(stack.size()).toBe(2) // Still 2 items + }) + + it('should report isEmpty correctly', () => { + const stack = new Stack() + + expect(stack.isEmpty()).toBe(true) + + stack.push(1) + expect(stack.isEmpty()).toBe(false) + + stack.pop() + expect(stack.isEmpty()).toBe(true) + }) + + it('should handle pop and peek on empty stack', () => { + const stack = new Stack() + + expect(stack.pop()).toBe(undefined) + expect(stack.peek()).toBe(undefined) + expect(stack.size()).toBe(0) + }) + + it('should solve valid parentheses problem', () => { + function isValid(s) { + const stack = [] + const pairs = { ')': '(', ']': '[', '}': '{' } + + for (const char of s) { + if (char in pairs) { + if (stack.pop() !== pairs[char]) { + return false + } + } else { + stack.push(char) + } + } + + return stack.length === 0 + } + + expect(isValid('()')).toBe(true) + expect(isValid('()[]{}')).toBe(true) + expect(isValid('([{}])')).toBe(true) + expect(isValid('(]')).toBe(false) + expect(isValid('([)]')).toBe(false) + expect(isValid('(((')).toBe(false) + }) + }) + + describe('Queue Implementation', () => { + class Queue { + constructor() { + this.items = [] + } + + enqueue(item) { + this.items.push(item) + } + + dequeue() { + return this.items.shift() + } + + front() { + return this.items[0] + } + + isEmpty() { + return this.items.length === 0 + } + + size() { + return this.items.length + } + } + + it('should follow FIFO (First In, First Out)', () => { + const queue = new Queue() + + queue.enqueue(1) + queue.enqueue(2) + queue.enqueue(3) + + expect(queue.dequeue()).toBe(1) // First in + expect(queue.dequeue()).toBe(2) + expect(queue.dequeue()).toBe(3) // Last in + }) + + it('should peek at front without removing', () => { + const queue = new Queue() + queue.enqueue('first') + queue.enqueue('second') + + expect(queue.front()).toBe('first') + expect(queue.size()).toBe(2) // Still 2 items + }) + + it('should report isEmpty correctly', () => { + const queue = new Queue() + + expect(queue.isEmpty()).toBe(true) + + queue.enqueue(1) + expect(queue.isEmpty()).toBe(false) + + queue.dequeue() + expect(queue.isEmpty()).toBe(true) + }) + + it('should handle dequeue and front on empty queue', () => { + const queue = new Queue() + + expect(queue.dequeue()).toBe(undefined) + expect(queue.front()).toBe(undefined) + expect(queue.size()).toBe(0) + }) + }) + + describe('Linked List Implementation', () => { + class Node { + constructor(value) { + this.value = value + this.next = null + } + } + + class LinkedList { + constructor() { + this.head = null + this.size = 0 + } + + prepend(value) { + const node = new Node(value) + node.next = this.head + this.head = node + this.size++ + } + + append(value) { + const node = new Node(value) + + if (!this.head) { + this.head = node + } else { + let current = this.head + while (current.next) { + current = current.next + } + current.next = node + } + this.size++ + } + + find(value) { + let current = this.head + while (current) { + if (current.value === value) { + return current + } + current = current.next + } + return null + } + + toArray() { + const result = [] + let current = this.head + while (current) { + result.push(current.value) + current = current.next + } + return result + } + } + + it('should prepend elements in O(1)', () => { + const list = new LinkedList() + + list.prepend(3) + list.prepend(2) + list.prepend(1) + + expect(list.toArray()).toEqual([1, 2, 3]) + }) + + it('should append elements', () => { + const list = new LinkedList() + + list.append(1) + list.append(2) + list.append(3) + + expect(list.toArray()).toEqual([1, 2, 3]) + }) + + it('should find elements', () => { + const list = new LinkedList() + list.append(1) + list.append(2) + list.append(3) + + const found = list.find(2) + expect(found.value).toBe(2) + expect(found.next.value).toBe(3) + + expect(list.find(5)).toBe(null) + }) + + it('should track size correctly', () => { + const list = new LinkedList() + + expect(list.size).toBe(0) + + list.append(1) + list.append(2) + list.prepend(0) + + expect(list.size).toBe(3) + }) + + it('should handle operations on empty list', () => { + const list = new LinkedList() + + expect(list.head).toBe(null) + expect(list.find(1)).toBe(null) + expect(list.toArray()).toEqual([]) + }) + + it('should reverse a linked list', () => { + function reverseList(head) { + let prev = null + let current = head + + while (current) { + const next = current.next + current.next = prev + prev = current + current = next + } + + return prev + } + + const list = new LinkedList() + list.append(1) + list.append(2) + list.append(3) + + list.head = reverseList(list.head) + expect(list.toArray()).toEqual([3, 2, 1]) + }) + }) + + describe('Binary Search Tree Implementation', () => { + class TreeNode { + constructor(value) { + this.value = value + this.left = null + this.right = null + } + } + + class BinarySearchTree { + constructor() { + this.root = null + } + + insert(value) { + const node = new TreeNode(value) + + if (!this.root) { + this.root = node + return + } + + let current = this.root + while (true) { + if (value < current.value) { + if (!current.left) { + current.left = node + return + } + current = current.left + } else { + if (!current.right) { + current.right = node + return + } + current = current.right + } + } + } + + search(value) { + let current = this.root + + while (current) { + if (value === current.value) { + return current + } + current = value < current.value ? current.left : current.right + } + + return null + } + + inOrder(node = this.root, result = []) { + if (node) { + this.inOrder(node.left, result) + result.push(node.value) + this.inOrder(node.right, result) + } + return result + } + } + + it('should insert values following BST property', () => { + const bst = new BinarySearchTree() + bst.insert(10) + bst.insert(5) + bst.insert(15) + + expect(bst.root.value).toBe(10) + expect(bst.root.left.value).toBe(5) + expect(bst.root.right.value).toBe(15) + }) + + it('should search for values', () => { + const bst = new BinarySearchTree() + bst.insert(10) + bst.insert(5) + bst.insert(15) + bst.insert(3) + bst.insert(7) + + expect(bst.search(7).value).toBe(7) + expect(bst.search(15).value).toBe(15) + expect(bst.search(100)).toBe(null) + }) + + it('should return sorted values with in-order traversal', () => { + const bst = new BinarySearchTree() + bst.insert(10) + bst.insert(5) + bst.insert(15) + bst.insert(3) + bst.insert(7) + bst.insert(20) + + expect(bst.inOrder()).toEqual([3, 5, 7, 10, 15, 20]) + }) + + it('should find max depth of tree', () => { + function maxDepth(root) { + if (!root) return 0 + + const leftDepth = maxDepth(root.left) + const rightDepth = maxDepth(root.right) + + return Math.max(leftDepth, rightDepth) + 1 + } + + const bst = new BinarySearchTree() + bst.insert(10) + bst.insert(5) + bst.insert(15) + bst.insert(3) + + expect(maxDepth(bst.root)).toBe(3) + }) + + it('should handle empty tree operations', () => { + const bst = new BinarySearchTree() + + expect(bst.root).toBe(null) + expect(bst.search(10)).toBe(null) + expect(bst.inOrder()).toEqual([]) + }) + + it('should handle duplicate values (goes to right subtree)', () => { + const bst = new BinarySearchTree() + bst.insert(10) + bst.insert(10) // Duplicate + bst.insert(10) // Another duplicate + + // Duplicates go to the right (based on our implementation: else branch) + expect(bst.root.value).toBe(10) + expect(bst.root.right.value).toBe(10) + expect(bst.root.right.right.value).toBe(10) + expect(bst.inOrder()).toEqual([10, 10, 10]) + }) + }) + + describe('Graph Implementation', () => { + class Graph { + constructor() { + this.adjacencyList = new Map() + } + + addVertex(vertex) { + if (!this.adjacencyList.has(vertex)) { + this.adjacencyList.set(vertex, []) + } + } + + addEdge(v1, v2) { + this.adjacencyList.get(v1).push(v2) + this.adjacencyList.get(v2).push(v1) + } + + bfs(start) { + const visited = new Set() + const queue = [start] + const result = [] + + while (queue.length) { + const vertex = queue.shift() + if (visited.has(vertex)) continue + + visited.add(vertex) + result.push(vertex) + + for (const neighbor of this.adjacencyList.get(vertex)) { + if (!visited.has(neighbor)) { + queue.push(neighbor) + } + } + } + + return result + } + + dfs(start, visited = new Set(), result = []) { + if (visited.has(start)) return result + + visited.add(start) + result.push(start) + + for (const neighbor of this.adjacencyList.get(start)) { + this.dfs(neighbor, visited, result) + } + + return result + } + } + + it('should add vertices and edges', () => { + const graph = new Graph() + graph.addVertex('A') + graph.addVertex('B') + graph.addVertex('C') + graph.addEdge('A', 'B') + graph.addEdge('A', 'C') + + expect(graph.adjacencyList.get('A')).toContain('B') + expect(graph.adjacencyList.get('A')).toContain('C') + expect(graph.adjacencyList.get('B')).toContain('A') + }) + + it('should perform breadth-first search', () => { + const graph = new Graph() + graph.addVertex('A') + graph.addVertex('B') + graph.addVertex('C') + graph.addVertex('D') + graph.addEdge('A', 'B') + graph.addEdge('A', 'C') + graph.addEdge('B', 'D') + + const result = graph.bfs('A') + + // BFS visits level by level + expect(result[0]).toBe('A') + expect(result.includes('B')).toBe(true) + expect(result.includes('C')).toBe(true) + expect(result.includes('D')).toBe(true) + }) + + it('should perform depth-first search', () => { + const graph = new Graph() + graph.addVertex('A') + graph.addVertex('B') + graph.addVertex('C') + graph.addVertex('D') + graph.addEdge('A', 'B') + graph.addEdge('A', 'C') + graph.addEdge('B', 'D') + + const result = graph.dfs('A') + + // DFS goes deep before wide + expect(result[0]).toBe('A') + expect(result.length).toBe(4) + }) + }) + + describe('Common Interview Patterns', () => { + it('Two Sum - using Map for O(n) lookup', () => { + function twoSum(nums, target) { + const seen = new Map() + + for (let i = 0; i < nums.length; i++) { + const complement = target - nums[i] + + if (seen.has(complement)) { + return [seen.get(complement), i] + } + + seen.set(nums[i], i) + } + + return [] + } + + expect(twoSum([2, 7, 11, 15], 9)).toEqual([0, 1]) + expect(twoSum([3, 2, 4], 6)).toEqual([1, 2]) + expect(twoSum([3, 3], 6)).toEqual([0, 1]) + }) + + it('Detect cycle in linked list - Floyd\'s algorithm', () => { + function hasCycle(head) { + let slow = head + let fast = head + + while (fast && fast.next) { + slow = slow.next + fast = fast.next.next + + if (slow === fast) { + return true + } + } + + return false + } + + // Create a list with cycle + const node1 = { val: 1, next: null } + const node2 = { val: 2, next: null } + const node3 = { val: 3, next: null } + node1.next = node2 + node2.next = node3 + node3.next = node1 // Cycle back to node1 + + expect(hasCycle(node1)).toBe(true) + + // List without cycle + const a = { val: 1, next: null } + const b = { val: 2, next: null } + a.next = b + + expect(hasCycle(a)).toBe(false) + }) + + it('Queue using two stacks', () => { + class QueueFromStacks { + constructor() { + this.stack1 = [] + this.stack2 = [] + } + + enqueue(item) { + this.stack1.push(item) + } + + dequeue() { + if (this.stack2.length === 0) { + while (this.stack1.length) { + this.stack2.push(this.stack1.pop()) + } + } + return this.stack2.pop() + } + } + + const queue = new QueueFromStacks() + queue.enqueue(1) + queue.enqueue(2) + queue.enqueue(3) + + expect(queue.dequeue()).toBe(1) // FIFO + expect(queue.dequeue()).toBe(2) + + queue.enqueue(4) + expect(queue.dequeue()).toBe(3) + expect(queue.dequeue()).toBe(4) + }) + }) + + describe('Choosing the Right Data Structure', () => { + it('should use Array for ordered data with index access', () => { + const todos = ['Buy milk', 'Walk dog', 'Write code'] + + // O(1) access by index + expect(todos[1]).toBe('Walk dog') + + // Easy to iterate + expect(todos.map(t => t.toUpperCase())).toEqual([ + 'BUY MILK', 'WALK DOG', 'WRITE CODE' + ]) + }) + + it('should use Set for unique values and fast lookup', () => { + const visited = new Set() + + // Track unique visitors + visited.add('user1') + visited.add('user2') + visited.add('user1') // Duplicate ignored + + expect(visited.size).toBe(2) + expect(visited.has('user1')).toBe(true) // O(1) lookup + }) + + it('should use Map for non-string keys or frequent updates', () => { + // Using objects as keys + const cache = new Map() + const request1 = { url: '/api/users', method: 'GET' } + const request2 = { url: '/api/posts', method: 'GET' } + + cache.set(request1, { data: ['user1', 'user2'] }) + cache.set(request2, { data: ['post1', 'post2'] }) + + expect(cache.get(request1).data).toEqual(['user1', 'user2']) + }) + + it('should use Stack for undo/redo or backtracking', () => { + const history = [] + + // Record actions + history.push('action1') + history.push('action2') + history.push('action3') + + // Undo - pop most recent + const undone = history.pop() + expect(undone).toBe('action3') + }) + + it('should use Queue for task scheduling', () => { + const taskQueue = [] + + // Add tasks + taskQueue.push('task1') + taskQueue.push('task2') + taskQueue.push('task3') + + // Process in order + expect(taskQueue.shift()).toBe('task1') // First added + expect(taskQueue.shift()).toBe('task2') + }) + }) +}) diff --git a/tests/advanced-topics/design-patterns/design-patterns.test.js b/tests/advanced-topics/design-patterns/design-patterns.test.js new file mode 100644 index 00000000..24e1b023 --- /dev/null +++ b/tests/advanced-topics/design-patterns/design-patterns.test.js @@ -0,0 +1,600 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +describe('Design Patterns', () => { + describe('Module Pattern', () => { + it('should encapsulate private state using closures', () => { + // IIFE-based module pattern + const Counter = (function () { + let count = 0 // Private variable + + return { + increment() { + count++ + return count + }, + decrement() { + count-- + return count + }, + getCount() { + return count + } + } + })() + + expect(Counter.getCount()).toBe(0) + expect(Counter.increment()).toBe(1) + expect(Counter.increment()).toBe(2) + expect(Counter.decrement()).toBe(1) + + // Private variable is not accessible + expect(Counter.count).toBeUndefined() + }) + + it('should only expose public methods', () => { + const Module = (function () { + // Private function + function privateHelper(value) { + return value * 2 + } + + // Public API + return { + publicMethod(value) { + return privateHelper(value) + 10 + } + } + })() + + expect(Module.publicMethod(5)).toBe(20) // (5 * 2) + 10 + expect(Module.privateHelper).toBeUndefined() + }) + }) + + describe('Singleton Pattern', () => { + it('should return the same instance when created multiple times', () => { + let instance = null + + class Singleton { + constructor() { + if (instance) { + return instance + } + this.timestamp = Date.now() + instance = this + } + } + + const instance1 = new Singleton() + const instance2 = new Singleton() + + expect(instance1).toBe(instance2) + expect(instance1.timestamp).toBe(instance2.timestamp) + }) + + it('should prevent modification with Object.freeze', () => { + const Config = { + apiUrl: 'https://api.example.com', + timeout: 5000 + } + + Object.freeze(Config) + + // In strict mode (which Vitest uses), this throws an error + expect(() => { + Config.apiUrl = 'https://evil.com' + }).toThrow(TypeError) + + expect(() => { + Config.newProperty = 'test' + }).toThrow(TypeError) + + // Original values remain unchanged + expect(Config.apiUrl).toBe('https://api.example.com') + expect(Config.newProperty).toBeUndefined() + }) + + it('should demonstrate that ES modules behave like singletons', () => { + // Simulating ES module behavior + const createModule = () => { + const cache = new Map() + + return function getModule(name) { + if (!cache.has(name)) { + cache.set(name, { name, timestamp: Date.now() }) + } + return cache.get(name) + } + } + + const requireModule = createModule() + + const module1 = requireModule('config') + const module2 = requireModule('config') + + expect(module1).toBe(module2) + }) + }) + + describe('Factory Pattern', () => { + it('should create objects without using the new keyword', () => { + function createUser(name, role) { + return { + name, + role, + greet() { + return `Hi, I'm ${this.name}` + } + } + } + + const user = createUser('Alice', 'admin') + + expect(user.name).toBe('Alice') + expect(user.role).toBe('admin') + expect(user.greet()).toBe("Hi, I'm Alice") + }) + + it('should return different object instances', () => { + function createProduct(name) { + return { name, id: Math.random() } + } + + const product1 = createProduct('Widget') + const product2 = createProduct('Widget') + + expect(product1).not.toBe(product2) + expect(product1.id).not.toBe(product2.id) + }) + + it('should create different types based on input', () => { + function createNotification(type, message) { + const base = { message, timestamp: Date.now() } + + switch (type) { + case 'error': + return { ...base, type: 'error', color: 'red', icon: '❌' } + case 'success': + return { ...base, type: 'success', color: 'green', icon: '✓' } + case 'warning': + return { ...base, type: 'warning', color: 'yellow', icon: '⚠' } + default: + return { ...base, type: 'info', color: 'blue', icon: 'ℹ' } + } + } + + const error = createNotification('error', 'Failed!') + const success = createNotification('success', 'Done!') + const info = createNotification('unknown', 'Info') + + expect(error.color).toBe('red') + expect(success.color).toBe('green') + expect(info.type).toBe('info') + }) + }) + + describe('Observer Pattern', () => { + let observable + + beforeEach(() => { + observable = { + observers: [], + + subscribe(fn) { + this.observers.push(fn) + return () => { + this.observers = this.observers.filter((o) => o !== fn) + } + }, + + notify(data) { + this.observers.forEach((fn) => fn(data)) + } + } + }) + + it('should allow subscribing to events', () => { + const callback = vi.fn() + + observable.subscribe(callback) + observable.notify('test data') + + expect(callback).toHaveBeenCalledWith('test data') + expect(callback).toHaveBeenCalledTimes(1) + }) + + it('should notify all subscribers', () => { + const callback1 = vi.fn() + const callback2 = vi.fn() + const callback3 = vi.fn() + + observable.subscribe(callback1) + observable.subscribe(callback2) + observable.subscribe(callback3) + + observable.notify('event data') + + expect(callback1).toHaveBeenCalledWith('event data') + expect(callback2).toHaveBeenCalledWith('event data') + expect(callback3).toHaveBeenCalledWith('event data') + }) + + it('should allow unsubscribing', () => { + const callback = vi.fn() + + const unsubscribe = observable.subscribe(callback) + + observable.notify('first') + expect(callback).toHaveBeenCalledTimes(1) + + unsubscribe() + + observable.notify('second') + expect(callback).toHaveBeenCalledTimes(1) // Still 1, not called again + }) + + it('should handle multiple subscriptions and unsubscriptions', () => { + const callback1 = vi.fn() + const callback2 = vi.fn() + + const unsub1 = observable.subscribe(callback1) + observable.subscribe(callback2) + + observable.notify('test') + expect(callback1).toHaveBeenCalledTimes(1) + expect(callback2).toHaveBeenCalledTimes(1) + + unsub1() + + observable.notify('test2') + expect(callback1).toHaveBeenCalledTimes(1) // Not called again + expect(callback2).toHaveBeenCalledTimes(2) // Called again + }) + }) + + describe('Proxy Pattern', () => { + it('should intercept property access (get)', () => { + const target = { name: 'Alice', age: 25 } + const accessLog = [] + + const proxy = new Proxy(target, { + get(obj, prop) { + accessLog.push(prop) + return obj[prop] + } + }) + + expect(proxy.name).toBe('Alice') + expect(proxy.age).toBe(25) + expect(accessLog).toEqual(['name', 'age']) + }) + + it('should intercept property assignment (set)', () => { + const target = { count: 0 } + const setLog = [] + + const proxy = new Proxy(target, { + set(obj, prop, value) { + setLog.push({ prop, value }) + obj[prop] = value + return true + } + }) + + proxy.count = 5 + proxy.newProp = 'hello' + + expect(target.count).toBe(5) + expect(target.newProp).toBe('hello') + expect(setLog).toEqual([ + { prop: 'count', value: 5 }, + { prop: 'newProp', value: 'hello' } + ]) + }) + + it('should validate values on set', () => { + const user = { name: 'Alice', age: 25 } + + const validatedUser = new Proxy(user, { + set(obj, prop, value) { + if (prop === 'age') { + if (typeof value !== 'number') { + throw new TypeError('Age must be a number') + } + if (value < 0 || value > 150) { + throw new RangeError('Age must be between 0 and 150') + } + } + obj[prop] = value + return true + } + }) + + // Valid assignment + validatedUser.age = 30 + expect(validatedUser.age).toBe(30) + + // Invalid assignments + expect(() => { + validatedUser.age = 'thirty' + }).toThrow(TypeError) + + expect(() => { + validatedUser.age = -5 + }).toThrow(RangeError) + + expect(() => { + validatedUser.age = 200 + }).toThrow(RangeError) + }) + + it('should provide default values for missing properties', () => { + const target = { name: 'Alice' } + + const withDefaults = new Proxy(target, { + get(obj, prop) { + if (prop in obj) { + return obj[prop] + } + return `Default value for ${prop}` + } + }) + + expect(withDefaults.name).toBe('Alice') + expect(withDefaults.missing).toBe('Default value for missing') + }) + }) + + describe('Decorator Pattern', () => { + it('should add new methods to objects', () => { + const createBird = (name) => ({ + name, + chirp() { + return `${this.name} says chirp!` + } + }) + + const withFlying = (bird) => ({ + ...bird, + fly() { + return `${bird.name} is flying!` + } + }) + + const sparrow = withFlying(createBird('Sparrow')) + + expect(sparrow.chirp()).toBe('Sparrow says chirp!') + expect(sparrow.fly()).toBe('Sparrow is flying!') + }) + + it('should preserve original object properties', () => { + const original = { + name: 'Widget', + price: 100, + getInfo() { + return `${this.name}: $${this.price}` + } + } + + const withDiscount = (product, discountPercent) => ({ + ...product, + discount: discountPercent, + getDiscountedPrice() { + return product.price * (1 - discountPercent / 100) + } + }) + + const discounted = withDiscount(original, 20) + + expect(discounted.name).toBe('Widget') + expect(discounted.price).toBe(100) + expect(discounted.discount).toBe(20) + expect(discounted.getDiscountedPrice()).toBe(80) + }) + + it('should allow composing multiple decorators', () => { + const createCharacter = (name) => ({ + name, + abilities: [], + describe() { + return `${this.name} can: ${this.abilities.join(', ') || 'nothing yet'}` + } + }) + + const withSwimming = (character) => ({ + ...character, + abilities: [...character.abilities, 'swim'], + swim() { + return `${character.name} swims!` + } + }) + + const withFlying = (character) => ({ + ...character, + abilities: [...character.abilities, 'fly'], + fly() { + return `${character.name} flies!` + } + }) + + const withFireBreathing = (character) => ({ + ...character, + abilities: [...character.abilities, 'breathe fire'], + breatheFire() { + return `${character.name} breathes fire!` + } + }) + + // Compose decorators + const dragon = withFireBreathing(withFlying(createCharacter('Dragon'))) + + expect(dragon.abilities).toEqual(['fly', 'breathe fire']) + expect(dragon.fly()).toBe('Dragon flies!') + expect(dragon.breatheFire()).toBe('Dragon breathes fire!') + + // Different composition + const duck = withSwimming(withFlying(createCharacter('Duck'))) + + expect(duck.abilities).toEqual(['fly', 'swim']) + expect(duck.fly()).toBe('Duck flies!') + expect(duck.swim()).toBe('Duck swims!') + }) + + it('should work with function decorators', () => { + // Logging decorator + const withLogging = (fn) => { + return function (...args) { + const result = fn.apply(this, args) + return result + } + } + + // Timing decorator + const withTiming = (fn) => { + return function (...args) { + const start = Date.now() + const result = fn.apply(this, args) + const end = Date.now() + return { result, duration: end - start } + } + } + + const add = (a, b) => a + b + const timedAdd = withTiming(withLogging(add)) + + const output = timedAdd(2, 3) + + expect(output.result).toBe(5) + expect(typeof output.duration).toBe('number') + expect(output.duration).toBeGreaterThanOrEqual(0) + }) + + it('should implement memoization decorator', () => { + const withMemoization = (fn) => { + const cache = new Map() + + return function (...args) { + const key = JSON.stringify(args) + + if (cache.has(key)) { + return { value: cache.get(key), cached: true } + } + + const result = fn.apply(this, args) + cache.set(key, result) + return { value: result, cached: false } + } + } + + let callCount = 0 + const expensiveOperation = (n) => { + callCount++ + return n * n + } + + const memoized = withMemoization(expensiveOperation) + + const result1 = memoized(5) + expect(result1).toEqual({ value: 25, cached: false }) + expect(callCount).toBe(1) + + const result2 = memoized(5) + expect(result2).toEqual({ value: 25, cached: true }) + expect(callCount).toBe(1) // Not called again + + const result3 = memoized(10) + expect(result3).toEqual({ value: 100, cached: false }) + expect(callCount).toBe(2) // Called for new argument + }) + }) + + describe('Pattern Integration', () => { + it('should combine Observer and Singleton for a global event bus', () => { + // Singleton event bus using module pattern + const EventBus = (function () { + const events = new Map() + + return Object.freeze({ + on(event, callback) { + if (!events.has(event)) { + events.set(event, []) + } + events.get(event).push(callback) + }, + + off(event, callback) { + if (events.has(event)) { + const callbacks = events.get(event).filter((cb) => cb !== callback) + events.set(event, callbacks) + } + }, + + emit(event, data) { + if (events.has(event)) { + events.get(event).forEach((callback) => callback(data)) + } + } + }) + })() + + const handler1 = vi.fn() + const handler2 = vi.fn() + + EventBus.on('user:login', handler1) + EventBus.on('user:login', handler2) + + EventBus.emit('user:login', { userId: 123 }) + + expect(handler1).toHaveBeenCalledWith({ userId: 123 }) + expect(handler2).toHaveBeenCalledWith({ userId: 123 }) + + EventBus.off('user:login', handler1) + EventBus.emit('user:login', { userId: 456 }) + + expect(handler1).toHaveBeenCalledTimes(1) + expect(handler2).toHaveBeenCalledTimes(2) + }) + + it('should combine Factory and Decorator patterns', () => { + // Factory for creating base enemies + const createEnemy = (type) => { + const enemies = { + goblin: { name: 'Goblin', health: 50, damage: 10 }, + orc: { name: 'Orc', health: 100, damage: 20 }, + troll: { name: 'Troll', health: 200, damage: 30 } + } + return { ...enemies[type] } + } + + // Decorators for enemy modifiers + const withArmor = (enemy, armor) => ({ + ...enemy, + armor, + takeDamage(amount) { + return Math.max(0, amount - armor) + } + }) + + const withPoison = (enemy) => ({ + ...enemy, + poisonDamage: 5, + attack() { + return `${enemy.name} attacks for ${enemy.damage} + ${this.poisonDamage} poison!` + } + }) + + // Create decorated enemies + const armoredOrc = withArmor(createEnemy('orc'), 15) + const poisonGoblin = withPoison(createEnemy('goblin')) + const armoredPoisonTroll = withPoison(withArmor(createEnemy('troll'), 25)) + + expect(armoredOrc.armor).toBe(15) + expect(armoredOrc.takeDamage(30)).toBe(15) + + expect(poisonGoblin.attack()).toBe('Goblin attacks for 10 + 5 poison!') + + expect(armoredPoisonTroll.armor).toBe(25) + expect(armoredPoisonTroll.attack()).toBe('Troll attacks for 30 + 5 poison!') + }) + }) +}) diff --git a/tests/advanced-topics/error-handling/error-handling.test.js b/tests/advanced-topics/error-handling/error-handling.test.js new file mode 100644 index 00000000..f0b8efb3 --- /dev/null +++ b/tests/advanced-topics/error-handling/error-handling.test.js @@ -0,0 +1,902 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' + +describe('Error Handling', () => { + + // ============================================================ + // TRY/CATCH/FINALLY BASICS + // ============================================================ + + describe('try/catch/finally Basics', () => { + it('should catch errors thrown in try block', () => { + // From: The try block contains code that might throw an error + let caught = false + + try { + throw new Error('Test error') + } catch (error) { + caught = true + expect(error.message).toBe('Test error') + } + + expect(caught).toBe(true) + }) + + it('should skip catch block if no error occurs', () => { + const order = [] + + try { + order.push('try') + } catch (error) { + order.push('catch') + } + + expect(order).toEqual(['try']) + }) + + it('should stop try block execution at the error', () => { + // From: If an error occurs, execution immediately jumps to the catch block + const order = [] + + try { + order.push('before error') + throw new Error('stop') + order.push('after error') // Never runs + } catch (error) { + order.push('catch') + } + + expect(order).toEqual(['before error', 'catch']) + }) + + it('should always run finally block - success case', () => { + // From: The finally block always runs + const order = [] + + try { + order.push('try') + } catch (error) { + order.push('catch') + } finally { + order.push('finally') + } + + expect(order).toEqual(['try', 'finally']) + }) + + it('should always run finally block - error case', () => { + const order = [] + + try { + order.push('try') + throw new Error('test') + } catch (error) { + order.push('catch') + } finally { + order.push('finally') + } + + expect(order).toEqual(['try', 'catch', 'finally']) + }) + + it('should run finally even with return in try', () => { + // From: finally runs even with return + const order = [] + + function example() { + try { + order.push('try') + return 'from try' + } finally { + order.push('finally') + } + } + + const result = example() + + expect(result).toBe('from try') + expect(order).toEqual(['try', 'finally']) + }) + + it('should run finally even with return in catch', () => { + const order = [] + + function example() { + try { + throw new Error('test') + } catch (error) { + order.push('catch') + return 'from catch' + } finally { + order.push('finally') + } + } + + const result = example() + + expect(result).toBe('from catch') + expect(order).toEqual(['catch', 'finally']) + }) + + it('should support optional catch binding (ES2019+)', () => { + // From: Optional catch binding + let result = 'default' + + try { + JSON.parse('{ invalid json }') + } catch { + // No error parameter needed + result = 'caught' + } + + expect(result).toBe('caught') + }) + }) + + // ============================================================ + // THE ERROR OBJECT + // ============================================================ + + describe('The Error Object', () => { + it('should have name, message, and stack properties', () => { + // From: Error Properties table + try { + undefinedVariable + } catch (error) { + expect(error.name).toBe('ReferenceError') + expect(error.message).toContain('undefinedVariable') + expect(typeof error.stack).toBe('string') + expect(error.stack).toContain('ReferenceError') + } + }) + + it('should convert error to string as "name: message"', () => { + const error = new Error('Something went wrong') + + expect(String(error)).toBe('Error: Something went wrong') + }) + + it('should support error cause (ES2022+)', () => { + // From: Error chaining with cause + const originalError = new Error('Original error') + const wrappedError = new Error('Wrapped error', { cause: originalError }) + + expect(wrappedError.message).toBe('Wrapped error') + expect(wrappedError.cause).toBe(originalError) + expect(wrappedError.cause.message).toBe('Original error') + }) + }) + + // ============================================================ + // BUILT-IN ERROR TYPES + // ============================================================ + + describe('Built-in Error Types', () => { + it('should throw TypeError for wrong type operations', () => { + // From: TypeError - calling method on null + expect(() => { + const obj = null + obj.method() + }).toThrow(TypeError) + + // Calling non-function + expect(() => { + const notAFunction = 42 + notAFunction() + }).toThrow(TypeError) + }) + + it('should throw ReferenceError for undefined variables', () => { + // From: ReferenceError - using undefined variable + expect(() => { + undefinedVariableName + }).toThrow(ReferenceError) + }) + + it('should throw SyntaxError for invalid JSON', () => { + // From: SyntaxError - invalid JSON + expect(() => { + JSON.parse('{ name: "John" }') // Missing quotes around key + }).toThrow(SyntaxError) + + expect(() => { + JSON.parse('') // Empty string + }).toThrow(SyntaxError) + }) + + it('should throw RangeError for out-of-range values', () => { + // From: RangeError - value out of range + expect(() => { + new Array(-1) + }).toThrow(RangeError) + + expect(() => { + (1.5).toFixed(200) // Max is 100 + }).toThrow(RangeError) + + // From: 'x'.repeat(Infinity) example + expect(() => { + 'x'.repeat(Infinity) + }).toThrow(RangeError) + }) + + it('should use optional chaining to avoid TypeError', () => { + // From: Fix for TypeError - Check if values exist before using them + const user = null + + // Without optional chaining - throws TypeError + expect(() => { + user.name + }).toThrow(TypeError) + + // With optional chaining - returns undefined (no error) + expect(user?.name).toBeUndefined() + }) + + it('should throw URIError for bad URI encoding', () => { + // From: URIError - bad URI encoding + expect(() => { + decodeURIComponent('%') + }).toThrow(URIError) + }) + + it('should throw AggregateError when all promises reject', async () => { + // From: AggregateError - Promise.any() all reject + await expect( + Promise.any([ + Promise.reject(new Error('Error 1')), + Promise.reject(new Error('Error 2')), + Promise.reject(new Error('Error 3')) + ]) + ).rejects.toThrow(AggregateError) + + try { + await Promise.any([ + Promise.reject(new Error('Error 1')), + Promise.reject(new Error('Error 2')) + ]) + } catch (error) { + expect(error.name).toBe('AggregateError') + expect(error.errors).toHaveLength(2) + } + }) + }) + + // ============================================================ + // THE THROW STATEMENT + // ============================================================ + + describe('The throw Statement', () => { + it('should throw and catch custom errors', () => { + // From: The throw statement lets you create your own errors + function divide(a, b) { + if (b === 0) { + throw new Error('Cannot divide by zero') + } + return a / b + } + + expect(divide(10, 2)).toBe(5) + expect(() => divide(10, 0)).toThrow('Cannot divide by zero') + }) + + it('should demonstrate throwing non-Error types (bad practice)', () => { + // From: Always Throw Error Objects - BAD examples + // These all work but lack stack traces for debugging + + // Throwing a string - no stack trace + try { + throw 'Something went wrong' + } catch (error) { + expect(typeof error).toBe('string') + expect(error).toBe('Something went wrong') + expect(error.stack).toBeUndefined() + } + + // Throwing a number - no stack trace + try { + throw 404 + } catch (error) { + expect(typeof error).toBe('number') + expect(error).toBe(404) + expect(error.stack).toBeUndefined() + } + + // Throwing an object - no stack trace + try { + throw { message: 'Error' } + } catch (error) { + expect(typeof error).toBe('object') + expect(error.message).toBe('Error') + expect(error.stack).toBeUndefined() + } + }) + + it('should throw errors with proper type', () => { + // From: Always throw Error objects + function validateAge(age) { + if (typeof age !== 'number') { + throw new TypeError(`Expected number but got ${typeof age}`) + } + if (age < 0 || age > 150) { + throw new RangeError(`Age must be between 0 and 150, got ${age}`) + } + return true + } + + expect(validateAge(25)).toBe(true) + expect(() => validateAge('25')).toThrow(TypeError) + expect(() => validateAge(-5)).toThrow(RangeError) + }) + + it('should include stack trace when throwing Error objects', () => { + try { + throw new Error('Test') + } catch (error) { + expect(error.stack).toBeDefined() + expect(error.stack).toContain('Error: Test') + } + }) + + it('should create meaningful error messages with context', () => { + // From: Creating Meaningful Error Messages + + // Specific error message with details + const email = 'invalid-email' + const emailError = new Error(`Email address is invalid: missing @ symbol`) + expect(emailError.message).toBe('Email address is invalid: missing @ symbol') + + // TypeError with actual type in message + const value = 42 + const typeError = new TypeError(`Expected string but got ${typeof value}`) + expect(typeError.message).toBe('Expected string but got number') + expect(typeError.name).toBe('TypeError') + + // RangeError with actual value in message + const age = 200 + const rangeError = new RangeError(`Age must be between 0 and 150, got ${age}`) + expect(rangeError.message).toBe('Age must be between 0 and 150, got 200') + expect(rangeError.name).toBe('RangeError') + }) + }) + + // ============================================================ + // CUSTOM ERROR CLASSES + // ============================================================ + + describe('Custom Error Classes', () => { + it('should create custom error classes', () => { + // From: Custom error classes for better categorization + class ValidationError extends Error { + constructor(message) { + super(message) + this.name = 'ValidationError' + } + } + + const error = new ValidationError('Invalid email') + + expect(error.name).toBe('ValidationError') + expect(error.message).toBe('Invalid email') + expect(error instanceof ValidationError).toBe(true) + expect(error instanceof Error).toBe(true) + }) + + it('should support auto-naming pattern', () => { + // From: The Auto-Naming Pattern + class AppError extends Error { + constructor(message, options) { + super(message, options) + this.name = this.constructor.name + } + } + + class ValidationError extends AppError {} + class NetworkError extends AppError {} + + const validationError = new ValidationError('Bad input') + const networkError = new NetworkError('Connection failed') + + expect(validationError.name).toBe('ValidationError') + expect(networkError.name).toBe('NetworkError') + }) + + it('should add custom properties to error classes', () => { + class NetworkError extends Error { + constructor(message, statusCode) { + super(message) + this.name = 'NetworkError' + this.statusCode = statusCode + } + } + + const error = new NetworkError('Not found', 404) + + expect(error.message).toBe('Not found') + expect(error.statusCode).toBe(404) + }) + + it('should use instanceof for error type checking', () => { + // From: Using instanceof for Error Handling + class ValidationError extends Error { + constructor(message) { + super(message) + this.name = 'ValidationError' + } + } + + class NetworkError extends Error { + constructor(message) { + super(message) + this.name = 'NetworkError' + } + } + + const errors = [ + new ValidationError('Bad input'), + new NetworkError('Offline'), + new Error('Unknown') + ] + + const results = errors.map(error => { + if (error instanceof ValidationError) return 'validation' + if (error instanceof NetworkError) return 'network' + return 'unknown' + }) + + expect(results).toEqual(['validation', 'network', 'unknown']) + }) + + it('should chain errors with cause', () => { + // From: Error Chaining with cause + class DataLoadError extends Error { + constructor(message, options) { + super(message, options) + this.name = 'DataLoadError' + } + } + + const originalError = new Error('Network timeout') + const wrappedError = new DataLoadError('Failed to load user data', { + cause: originalError + }) + + expect(wrappedError.cause).toBe(originalError) + expect(wrappedError.cause.message).toBe('Network timeout') + }) + }) + + // ============================================================ + // ASYNC ERROR HANDLING + // ============================================================ + + describe('Async Error Handling', () => { + it('should catch Promise rejections with .catch()', async () => { + // From: With Promises: .catch() + const result = await Promise.reject(new Error('Failed')) + .catch(error => `Caught: ${error.message}`) + + expect(result).toBe('Caught: Failed') + }) + + it('should catch async/await errors with try/catch', async () => { + // From: With async/await: try/catch + async function failingOperation() { + throw new Error('Async failure') + } + + let caught = null + + try { + await failingOperation() + } catch (error) { + caught = error.message + } + + expect(caught).toBe('Async failure') + }) + + it('should run .finally() regardless of outcome', async () => { + const order = [] + + // Success case + await Promise.resolve('success') + .then(v => { order.push('then') }) + .finally(() => { order.push('finally-success') }) + + // Failure case + await Promise.reject(new Error('fail')) + .catch(e => { order.push('catch') }) + .finally(() => { order.push('finally-fail') }) + + expect(order).toEqual(['then', 'finally-success', 'catch', 'finally-fail']) + }) + + it('should demonstrate try/catch only catches synchronous errors', async () => { + // From: try/catch Only Works Synchronously + const order = [] + + try { + setTimeout(() => { + order.push('timeout executed') + // If we threw here, try/catch wouldn't catch it + }, 0) + order.push('after setTimeout') + } catch (error) { + order.push('catch') + } + + expect(order).toEqual(['after setTimeout']) + + // Let setTimeout execute + await new Promise(resolve => setTimeout(resolve, 10)) + + expect(order).toEqual(['after setTimeout', 'timeout executed']) + }) + }) + + // ============================================================ + // GLOBAL ERROR HANDLERS (Not Tested - Browser-Specific) + // ============================================================ + // + // The following patterns from the concept page are browser-specific + // and cannot be tested in a Node.js/Vitest environment: + // + // - window.onerror = function(message, source, lineno, colno, error) { ... } + // Catches uncaught synchronous errors in the browser + // + // - window.addEventListener('unhandledrejection', event => { ... }) + // Catches unhandled Promise rejections in the browser + // + // These are documented in the concept page (lines 531-557) for browser usage. + // In production, these are typically used for: + // - Logging errors to services like Sentry or LogRocket + // - Showing generic "something went wrong" messages + // - Tracking errors in production environments + // + // They should be used as a safety net, not as primary error handling. + // ============================================================ + + // ============================================================ + // COMMON MISTAKES + // ============================================================ + + describe('Common Mistakes', () => { + it('Mistake 1: Empty catch blocks swallow errors', () => { + // From: Empty catch Blocks (Swallowing Errors) + let errorLogged = false + + // Bad: error is silently swallowed + try { + throw new Error('Silent error') + } catch (error) { + // Empty - bad practice + } + + // Good: at least log the error + try { + throw new Error('Logged error') + } catch (error) { + errorLogged = true + // console.error('Error:', error) in real code + } + + expect(errorLogged).toBe(true) + }) + + it('Mistake 2: Catching too broadly hides bugs', () => { + // From: Catching Too Broadly + function parseWithBugHidden(input) { + try { + const result = JSON.parse(input) + // Bug: typo in variable name would be hidden + return result + } catch (error) { + return 'Something went wrong' + } + } + + function parseCorrectly(input) { + try { + return JSON.parse(input) + } catch (error) { + if (error instanceof SyntaxError) { + return null // Expected case + } + throw error // Unexpected: re-throw + } + } + + expect(parseWithBugHidden('{ invalid }')).toBe('Something went wrong') + expect(parseCorrectly('{ invalid }')).toBe(null) + expect(parseCorrectly('{"valid": true}')).toEqual({ valid: true }) + }) + + it('Mistake 3: Throwing strings instead of Error objects', () => { + // From: Throwing Strings Instead of Errors + + // Bad: no stack trace + try { + throw 'String error' + } catch (error) { + expect(typeof error).toBe('string') + expect(error.stack).toBeUndefined() + } + + // Good: has stack trace + try { + throw new Error('Error object') + } catch (error) { + expect(error instanceof Error).toBe(true) + expect(error.stack).toBeDefined() + } + }) + + it('Mistake 4: Not re-throwing when needed', async () => { + // From: Not Re-throwing When Needed + + // Bad: returns undefined, caller thinks success + async function badFetch() { + try { + throw new Error('Network error') + } catch (error) { + // Just logs, doesn't re-throw or return meaningful value + } + } + + // Good: re-throws for caller to handle + async function goodFetch() { + try { + throw new Error('Network error') + } catch (error) { + throw error // Let caller decide what to do + } + } + + const badResult = await badFetch() + expect(badResult).toBeUndefined() // Silent failure! + + await expect(goodFetch()).rejects.toThrow('Network error') + }) + + it('Mistake 5: Expecting try/catch to catch async callback errors', async () => { + // From: Forgetting try/catch is Synchronous + let syncCaughtError = null + let callbackError = null + + // This catch won't catch the setTimeout error + try { + setTimeout(() => { + try { + throw new Error('Callback error') + } catch (e) { + callbackError = e.message + } + }, 0) + } catch (error) { + syncCaughtError = error.message + } + + expect(syncCaughtError).toBeNull() // Sync catch doesn't catch async + + await new Promise(resolve => setTimeout(resolve, 10)) + + expect(callbackError).toBe('Callback error') // Inner catch works + }) + }) + + // ============================================================ + // REAL-WORLD PATTERNS + // ============================================================ + + describe('Real-World Patterns', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + it('should implement retry pattern', async () => { + // From: Retry Pattern + let attempts = 0 + + async function flakyOperation() { + attempts++ + if (attempts < 3) { + throw new Error('Temporary failure') + } + return 'success' + } + + async function fetchWithRetry(operation, retries = 3) { + for (let i = 0; i < retries; i++) { + try { + return await operation() + } catch (error) { + if (i === retries - 1) throw error + await new Promise(r => setTimeout(r, 100 * Math.pow(2, i))) + } + } + } + + const promise = fetchWithRetry(flakyOperation, 3) + + // First attempt fails + await vi.advanceTimersByTimeAsync(0) + expect(attempts).toBe(1) + + // Wait for first retry (100ms) + await vi.advanceTimersByTimeAsync(100) + expect(attempts).toBe(2) + + // Wait for second retry (200ms) + await vi.advanceTimersByTimeAsync(200) + expect(attempts).toBe(3) + + const result = await promise + expect(result).toBe('success') + }) + + it('should implement validation error pattern', () => { + // From: Validation Error Pattern + class ValidationError extends Error { + constructor(errors) { + super('Validation failed') + this.name = 'ValidationError' + this.errors = errors + } + } + + function validateUser(data) { + const errors = {} + + if (!data.email?.includes('@')) { + errors.email = 'Invalid email address' + } + if (data.age < 0) { + errors.age = 'Age must be positive' + } + + if (Object.keys(errors).length > 0) { + throw new ValidationError(errors) + } + + return true + } + + // Valid data + expect(validateUser({ email: 'test@example.com', age: 25 })).toBe(true) + + // Invalid data + try { + validateUser({ email: 'invalid', age: -5 }) + } catch (error) { + expect(error instanceof ValidationError).toBe(true) + expect(error.errors.email).toBe('Invalid email address') + expect(error.errors.age).toBe('Age must be positive') + } + }) + + it('should implement graceful degradation', async () => { + // From: Graceful Degradation + let apiCalled = false + let cacheCalled = false + + async function fetchFromApi() { + apiCalled = true + throw new Error('API unavailable') + } + + async function loadFromCache() { + cacheCalled = true + return { cached: true } + } + + async function loadUserPreferences() { + try { + return await fetchFromApi() + } catch (apiError) { + try { + return await loadFromCache() + } catch (cacheError) { + return { theme: 'light', language: 'en' } // Defaults + } + } + } + + const result = await loadUserPreferences() + + expect(apiCalled).toBe(true) + expect(cacheCalled).toBe(true) + expect(result).toEqual({ cached: true }) + }) + }) + + // ============================================================ + // RETHROWING ERRORS + // ============================================================ + + describe('Rethrowing Errors', () => { + it('should rethrow errors you cannot handle', () => { + // From: Catch should only process errors that it knows + function parseUserData(json) { + try { + return JSON.parse(json) + } catch (error) { + if (error instanceof SyntaxError) { + // We know how to handle this + return null + } + // Unknown error, rethrow + throw error + } + } + + expect(parseUserData('{"name": "John"}')).toEqual({ name: 'John' }) + expect(parseUserData('invalid')).toBeNull() + }) + + it('should wrap errors with additional context', () => { + function processOrder(order) { + try { + if (!order.items) { + throw new Error('Order has no items') + } + return { processed: true } + } catch (error) { + throw new Error(`Failed to process order ${order.id}`, { cause: error }) + } + } + + try { + processOrder({ id: '123' }) + } catch (error) { + expect(error.message).toBe('Failed to process order 123') + expect(error.cause.message).toBe('Order has no items') + } + }) + }) + + // ============================================================ + // SCOPING IN TRY/CATCH + // ============================================================ + + describe('Variable Scoping in try/catch', () => { + it('should demonstrate variable scoping issue', () => { + // From: Test Your Knowledge Question 6 + + // Wrong: result is scoped to try block + let wrongResult + try { + const result = 'value' + wrongResult = result + } catch (e) { + // handle error + } + // console.log(result) would throw ReferenceError + + // Correct: declare outside try block + let correctResult + try { + correctResult = 'value' + } catch (e) { + correctResult = 'fallback' + } + + expect(correctResult).toBe('value') + }) + + it('should use fallback value when error occurs', () => { + let result + + try { + result = JSON.parse('invalid') + } catch (e) { + result = { fallback: true } + } + + expect(result).toEqual({ fallback: true }) + }) + }) +}) diff --git a/tests/advanced-topics/es-modules/es-modules.test.js b/tests/advanced-topics/es-modules/es-modules.test.js new file mode 100644 index 00000000..d62bfac8 --- /dev/null +++ b/tests/advanced-topics/es-modules/es-modules.test.js @@ -0,0 +1,1207 @@ +import { describe, it, expect, vi } from 'vitest' + +describe('ES Modules', () => { + // =========================================== + // Part 1: Live Bindings + // =========================================== + + describe('Part 1: Live Bindings', () => { + describe('ESM Live Bindings vs CommonJS Value Copies', () => { + it('should demonstrate CommonJS-style value copy behavior', () => { + // CommonJS exports copies of primitive values at require time + // Simulating: module.exports = { count, increment, getCount } + + function createCommonJSModule() { + let count = 0 + function increment() { count++ } + function getCount() { return count } + + // CommonJS exports a snapshot (copy) of the value + return { count, increment, getCount } + } + + const { count, increment, getCount } = createCommonJSModule() + + expect(count).toBe(0) + increment() + expect(count).toBe(0) // Still 0! It's a copy from export time + expect(getCount()).toBe(1) // Function reads the real internal value + }) + + it('should demonstrate ESM-style live binding behavior', () => { + // ESM exports live references - changes are visible to importers + // Simulating with an object that acts as a module namespace + + function createESMModule() { + const moduleNamespace = { + count: 0, + increment() { + moduleNamespace.count++ + } + } + return moduleNamespace + } + + const mod = createESMModule() + + expect(mod.count).toBe(0) + mod.increment() + expect(mod.count).toBe(1) // Live binding reflects the change! + mod.increment() + expect(mod.count).toBe(2) // Still updating + }) + + it('should show live bindings work with objects', () => { + // Even with objects, ESM bindings are live references + const moduleState = { + user: null, + setUser(u) { moduleState.user = u }, + getUser() { return moduleState.user } + } + + expect(moduleState.user).toBe(null) + + moduleState.setUser({ name: 'Alice' }) + expect(moduleState.user).toEqual({ name: 'Alice' }) // Live! + + moduleState.setUser({ name: 'Bob' }) + expect(moduleState.user).toEqual({ name: 'Bob' }) // Updated! + }) + + it('should demonstrate singleton state via live bindings', () => { + // All importers share the same module state + const sharedModule = (() => { + let state = { count: 0 } + return { + getState: () => state, + increment: () => { state.count++ } + } + })() + + // Simulate two different "importers" + const importer1 = sharedModule + const importer2 = sharedModule + + importer1.increment() + expect(importer1.getState().count).toBe(1) + expect(importer2.getState().count).toBe(1) // Same state! + + importer2.increment() + expect(importer1.getState().count).toBe(2) // Both see the update + expect(importer2.getState().count).toBe(2) + }) + }) + + describe('Why Live Bindings Matter', () => { + it('should enable proper state management across module boundaries', () => { + // Auth module that multiple parts of an app might import + const authModule = (() => { + let currentUser = null + let isAuthenticated = false + + return { + get currentUser() { return currentUser }, + get isAuthenticated() { return isAuthenticated }, + login(user) { + currentUser = user + isAuthenticated = true + }, + logout() { + currentUser = null + isAuthenticated = false + } + } + })() + + // Header component checks auth + expect(authModule.isAuthenticated).toBe(false) + + // Login form logs in + authModule.login({ name: 'Alice', email: 'alice@test.com' }) + + // Header immediately sees the change (live binding) + expect(authModule.isAuthenticated).toBe(true) + expect(authModule.currentUser.name).toBe('Alice') + + // Logout button logs out + authModule.logout() + + // All components see the change + expect(authModule.isAuthenticated).toBe(false) + expect(authModule.currentUser).toBe(null) + }) + }) + }) + + // =========================================== + // Part 2: Read-Only Imports + // =========================================== + + describe('Part 2: Read-Only Imports', () => { + describe('Imported bindings cannot be reassigned', () => { + it('should demonstrate that imports are read-only (simulated with Object.defineProperty)', () => { + // ESM imports are read-only - you can't reassign them + // We simulate this with a frozen/non-writable property + + const moduleExports = {} + Object.defineProperty(moduleExports, 'count', { + value: 0, + writable: false, + enumerable: true + }) + + expect(moduleExports.count).toBe(0) + + // Attempting to reassign throws in strict mode + expect(() => { + 'use strict' + moduleExports.count = 10 + }).toThrow(TypeError) + }) + + it('should show that const-like behavior applies to all imports', () => { + // Even if the source uses `let`, importers can't reassign + const createModule = () => { + let value = 'original' // let in source module + return { + get value() { return value }, + setValue(v) { value = v } // only module can change it + } + } + + const mod = createModule() + + // Importer can read + expect(mod.value).toBe('original') + + // Importer can call methods that modify (module modifies itself) + mod.setValue('updated') + expect(mod.value).toBe('updated') + + // But direct assignment to the binding would fail in real ESM + // import { value } from './mod.js' + // value = 'hack' // TypeError: Assignment to constant variable + }) + + it('should allow modification of imported object properties', () => { + // You can't reassign the import, but you CAN modify object properties + const configModule = { + config: { + theme: 'light', + debug: false + } + } + + // Can't do: config = newObject (would throw) + // But CAN do: config.theme = 'dark' + + configModule.config.theme = 'dark' + expect(configModule.config.theme).toBe('dark') + + configModule.config.debug = true + expect(configModule.config.debug).toBe(true) + }) + }) + }) + + // =========================================== + // Part 3: Circular Dependencies and TDZ + // =========================================== + + describe('Part 3: Circular Dependencies and TDZ', () => { + describe('Temporal Dead Zone (TDZ) with const/let', () => { + it('should throw ReferenceError when accessing const before initialization', () => { + expect(() => { + // This simulates what happens in a circular dependency + // when module B tries to access a const from module A + // before A has finished executing + + const accessBeforeInit = () => { + console.log(value) // Accessing before declaration + const value = 'initialized' + } + accessBeforeInit() + }).toThrow(ReferenceError) + }) + + it('should throw ReferenceError when accessing let before initialization', () => { + expect(() => { + const accessBeforeInit = () => { + console.log(value) // TDZ - ReferenceError + let value = 'initialized' + } + accessBeforeInit() + }).toThrow(ReferenceError) + }) + + it('should NOT throw with var (hoisted with undefined)', () => { + // var is hoisted and initialized to undefined + // This is why old circular dependency examples showed 'undefined' + let result + + const accessVarBeforeInit = () => { + result = value // undefined, not an error + var value = 'initialized' + } + + accessVarBeforeInit() + expect(result).toBe(undefined) // var is hoisted as undefined + }) + }) + + describe('Circular Dependency Patterns', () => { + it('should demonstrate safe circular dependency with deferred access', () => { + // Safe pattern: export functions that access values at call time + + const moduleA = { + value: null, + getValue: () => moduleA.value, + init: () => { moduleA.value = 'A initialized' } + } + + const moduleB = { + value: null, + getValue: () => moduleB.value, + getAValue: () => moduleA.getValue(), // Deferred access + init: () => { moduleB.value = 'B initialized' } + } + + // Simulate circular initialization + // B tries to access A.value before A.init() runs + expect(moduleB.getAValue()).toBe(null) // A not initialized yet + + moduleA.init() + expect(moduleB.getAValue()).toBe('A initialized') // Now it works + + moduleB.init() + expect(moduleB.getValue()).toBe('B initialized') + }) + + it('should show how to restructure to avoid circular deps', () => { + // Instead of A importing B and B importing A, + // create a shared module C that both import + + const sharedModule = { + sharedConfig: { apiUrl: 'https://api.example.com' }, + sharedUtil: (x) => x.toUpperCase() + } + + const moduleA = { + config: sharedModule.sharedConfig, + formatName: (name) => sharedModule.sharedUtil(name) + } + + const moduleB = { + config: sharedModule.sharedConfig, + formatTitle: (title) => sharedModule.sharedUtil(title) + } + + // No circular dependency - both depend on shared module + expect(moduleA.formatName('alice')).toBe('ALICE') + expect(moduleB.formatTitle('hello')).toBe('HELLO') + expect(moduleA.config).toBe(moduleB.config) // Same reference + }) + }) + }) + + // =========================================== + // Part 4: Module Singleton Behavior + // =========================================== + + describe('Part 4: Module Singleton Behavior', () => { + describe('Module code executes exactly once', () => { + it('should only run initialization code once', () => { + let initCount = 0 + + // Simulating a module that runs initialization code + const createSingletonModule = (() => { + initCount++ // This runs once when module loads + + return { + getValue: () => 'module value', + getInitCount: () => initCount + } + })() + + // Multiple "imports" all get the same instance + const import1 = createSingletonModule + const import2 = createSingletonModule + const import3 = createSingletonModule + + expect(initCount).toBe(1) // Only ran once + expect(import1).toBe(import2) + expect(import2).toBe(import3) + }) + + it('should share state across all importers', () => { + const cacheModule = (() => { + const cache = new Map() + console.log('Cache module initialized') // Runs once + + return { + set: (key, value) => cache.set(key, value), + get: (key) => cache.get(key), + size: () => cache.size + } + })() + + // Different "files" using the cache + // file1.js + cacheModule.set('user', { id: 1 }) + + // file2.js - sees the same cache + expect(cacheModule.get('user')).toEqual({ id: 1 }) + + // file3.js - also same cache + cacheModule.set('token', 'abc123') + + expect(cacheModule.size()).toBe(2) + }) + + it('should maintain singleton even with different import styles', () => { + // Whether you use named imports, default import, or namespace import, + // you get the same module instance + + const mathModule = (() => { + const moduleId = Math.random() // Generated once + + return { + moduleId, + PI: 3.14159, + add: (a, b) => a + b, + default: function Calculator() { + this.result = 0 + } + } + })() + + // import { add, PI } from './math.js' + const { add, PI } = mathModule + + // import * as math from './math.js' + const math = mathModule + + // import Calculator from './math.js' + const Calculator = mathModule.default + + // All reference the same module + expect(math.PI).toBe(PI) + expect(math.add).toBe(add) + expect(math.moduleId).toBe(mathModule.moduleId) + }) + }) + }) + + // =========================================== + // Part 5: Dynamic Imports + // =========================================== + + describe('Part 5: Dynamic Imports', () => { + describe('import() returns a Promise', () => { + it('should resolve to module namespace object', async () => { + // Simulating dynamic import behavior + const mockModule = { + namedExport: 'named value', + anotherExport: 42, + default: function DefaultExport() { return 'default' } + } + + const dynamicImport = () => Promise.resolve(mockModule) + + const module = await dynamicImport() + + expect(module.namedExport).toBe('named value') + expect(module.anotherExport).toBe(42) + expect(module.default()).toBe('default') + }) + + it('should allow destructuring named exports', async () => { + const mockDateModule = { + formatDate: (d) => d.toISOString(), + parseDate: (s) => new Date(s) + } + + const dynamicImport = () => Promise.resolve(mockDateModule) + + // Destructure directly from await + const { formatDate, parseDate } = await dynamicImport() + + expect(typeof formatDate).toBe('function') + expect(typeof parseDate).toBe('function') + }) + + it('should access default export via .default property', async () => { + const mockModule = { + default: class Logger { + log(msg) { return `[LOG] ${msg}` } + } + } + + const dynamicImport = () => Promise.resolve(mockModule) + + // Method 1: Destructure with rename + const { default: Logger } = await dynamicImport() + const logger1 = new Logger() + expect(logger1.log('test')).toBe('[LOG] test') + + // Method 2: Access .default property + const module = await dynamicImport() + const Logger2 = module.default + const logger2 = new Logger2() + expect(logger2.log('hello')).toBe('[LOG] hello') + }) + }) + + describe('Dynamic Import Use Cases', () => { + it('should enable conditional module loading', async () => { + const modules = { + light: { theme: 'light', bg: '#fff', text: '#000' }, + dark: { theme: 'dark', bg: '#000', text: '#fff' } + } + + async function loadTheme(themeName) { + // Simulating: const theme = await import(`./themes/${themeName}.js`) + return Promise.resolve(modules[themeName]) + } + + const lightTheme = await loadTheme('light') + expect(lightTheme.bg).toBe('#fff') + + const darkTheme = await loadTheme('dark') + expect(darkTheme.bg).toBe('#000') + }) + + it('should enable route-based code splitting', async () => { + const pageModules = { + home: { default: () => 'Home Page Content' }, + about: { default: () => 'About Page Content' }, + contact: { default: () => 'Contact Page Content' } + } + + async function loadPage(pageName) { + // Simulating route-based dynamic import + const pageModule = await Promise.resolve(pageModules[pageName]) + return pageModule.default + } + + const HomePage = await loadPage('home') + expect(HomePage()).toBe('Home Page Content') + + const AboutPage = await loadPage('about') + expect(AboutPage()).toBe('About Page Content') + }) + + it('should enable lazy loading of heavy features', async () => { + let chartLibraryLoaded = false + + const heavyChartLibrary = { + Chart: class { + constructor(data) { + chartLibraryLoaded = true + this.data = data + } + render() { + return `Chart with ${this.data.length} points` + } + } + } + + async function showChart(data) { + // Only load chart library when actually needed + const { Chart } = await Promise.resolve(heavyChartLibrary) + const chart = new Chart(data) + return chart.render() + } + + expect(chartLibraryLoaded).toBe(false) // Not loaded yet + + const result = await showChart([1, 2, 3, 4, 5]) + + expect(chartLibraryLoaded).toBe(true) // Now loaded + expect(result).toBe('Chart with 5 points') + }) + + it('should work with Promise.all for parallel loading', async () => { + const modules = { + header: { render: () => '<header>Header</header>' }, + footer: { render: () => '<footer>Footer</footer>' }, + sidebar: { render: () => '<aside>Sidebar</aside>' } + } + + async function loadComponents() { + const [header, footer, sidebar] = await Promise.all([ + Promise.resolve(modules.header), + Promise.resolve(modules.footer), + Promise.resolve(modules.sidebar) + ]) + + return { header, footer, sidebar } + } + + const components = await loadComponents() + + expect(components.header.render()).toBe('<header>Header</header>') + expect(components.footer.render()).toBe('<footer>Footer</footer>') + expect(components.sidebar.render()).toBe('<aside>Sidebar</aside>') + }) + }) + + describe('Error Handling with Dynamic Imports', () => { + it('should handle module not found errors', async () => { + const loadModule = (name) => { + if (name === 'nonexistent') { + return Promise.reject(new Error('Module not found')) + } + return Promise.resolve({ value: 'found' }) + } + + // Successful load + const mod = await loadModule('existing') + expect(mod.value).toBe('found') + + // Failed load + await expect(loadModule('nonexistent')).rejects.toThrow('Module not found') + }) + + it('should use try-catch for error handling', async () => { + const loadModule = () => Promise.reject(new Error('Network error')) + + let errorHandled = false + let fallbackUsed = false + + try { + await loadModule() + } catch (error) { + errorHandled = true + // Use fallback + fallbackUsed = true + } + + expect(errorHandled).toBe(true) + expect(fallbackUsed).toBe(true) + }) + }) + }) + + // =========================================== + // Part 6: Export and Import Syntax Variations + // =========================================== + + describe('Part 6: Export and Import Syntax Variations', () => { + describe('Named Exports', () => { + it('should support inline named exports', () => { + // export const PI = 3.14159 + // export function square(x) { return x * x } + // export class Circle { } + + const moduleExports = {} + + moduleExports.PI = 3.14159 + moduleExports.square = function(x) { return x * x } + moduleExports.Circle = class { + constructor(radius) { this.radius = radius } + area() { return moduleExports.PI * this.radius ** 2 } + } + + expect(moduleExports.PI).toBe(3.14159) + expect(moduleExports.square(4)).toBe(16) + + const circle = new moduleExports.Circle(5) + expect(circle.area()).toBeCloseTo(78.54, 1) + }) + + it('should support grouped exports at bottom', () => { + // const PI = 3.14159 + // function square(x) { return x * x } + // export { PI, square } + + const PI = 3.14159 + function square(x) { return x * x } + + const exports = { PI, square } + + expect(exports.PI).toBe(3.14159) + expect(exports.square(5)).toBe(25) + }) + + it('should support renaming exports with as', () => { + // function internalHelper() { } + // export { internalHelper as helper } + + function internalHelper() { return 'helped' } + function _privateUtil() { return 'util' } + + const exports = { + helper: internalHelper, + publicUtil: _privateUtil + } + + expect(exports.helper()).toBe('helped') + expect(exports.publicUtil()).toBe('util') + expect(exports.internalHelper).toBe(undefined) // Not exported under original name + }) + }) + + describe('Default Exports', () => { + it('should support default export of function', () => { + // export default function greet(name) { } + + function greet(name) { return `Hello, ${name}!` } + const moduleExports = { default: greet } + + // import greet from './greet.js' + const importedGreet = moduleExports.default + expect(importedGreet('World')).toBe('Hello, World!') + }) + + it('should support default export of class', () => { + // export default class User { } + + class User { + constructor(name) { this.name = name } + greet() { return `Hi, I'm ${this.name}` } + } + + const moduleExports = { default: User } + + // import User from './user.js' + const ImportedUser = moduleExports.default + const user = new ImportedUser('Alice') + expect(user.greet()).toBe("Hi, I'm Alice") + }) + + it('should support default export of object/value', () => { + // export default { name: 'Config', version: '1.0' } + + const moduleExports = { + default: { + name: 'Config', + version: '1.0.0', + debug: false + } + } + + // import config from './config.js' + const config = moduleExports.default + expect(config.name).toBe('Config') + expect(config.version).toBe('1.0.0') + }) + }) + + describe('Mixed Named and Default Exports', () => { + it('should support both default and named exports', () => { + // export default function React() { } + // export function useState() { } + // export function useEffect() { } + + function React() { return 'React' } + function useState(initial) { return [initial, () => {}] } + function useEffect(fn) { fn() } + + const moduleExports = { + default: React, + useState, + useEffect + } + + // import React, { useState, useEffect } from 'react' + const ImportedReact = moduleExports.default + const { useState: importedUseState, useEffect: importedUseEffect } = moduleExports + + expect(ImportedReact()).toBe('React') + expect(importedUseState(0)).toEqual([0, expect.any(Function)]) + }) + }) + + describe('Import Variations', () => { + it('should support named imports with exact names', () => { + // import { PI, square } from './math.js' + + const mathModule = { + PI: 3.14159, + square: (x) => x * x, + cube: (x) => x * x * x + } + + const { PI, square } = mathModule + + expect(PI).toBe(3.14159) + expect(square(3)).toBe(9) + }) + + it('should support renaming imports with as', () => { + // import { formatDate as formatDateISO } from './date.js' + + const dateModule = { + formatDate: (d) => d.toISOString() + } + + const dateUSModule = { + formatDate: (d) => d.toLocaleDateString('en-US') + } + + const { formatDate: formatDateISO } = dateModule + const { formatDate: formatDateUS } = dateUSModule + + const date = new Date('2024-01-15') + expect(formatDateISO(date)).toContain('2024-01-15') + expect(typeof formatDateUS(date)).toBe('string') + }) + + it('should support namespace imports (import * as)', () => { + // import * as math from './math.js' + + const mathModule = { + PI: 3.14159, + E: 2.71828, + add: (a, b) => a + b, + multiply: (a, b) => a * b, + default: { name: 'Math Utils' } + } + + // Namespace import gets all exports as properties + const math = mathModule + + expect(math.PI).toBe(3.14159) + expect(math.E).toBe(2.71828) + expect(math.add(2, 3)).toBe(5) + expect(math.multiply(4, 5)).toBe(20) + expect(math.default.name).toBe('Math Utils') // default is also accessible + }) + + it('should support side-effect only imports', () => { + // import './polyfills.js' + // import './analytics.js' + + let polyfillsLoaded = false + let analyticsInitialized = false + + // Simulating side-effect modules + const loadPolyfills = () => { polyfillsLoaded = true } + const initAnalytics = () => { analyticsInitialized = true } + + loadPolyfills() + initAnalytics() + + expect(polyfillsLoaded).toBe(true) + expect(analyticsInitialized).toBe(true) + }) + }) + + describe('Re-exports (Barrel Files)', () => { + it('should support re-exporting named exports', () => { + // date.js + const dateModule = { + formatDate: (d) => d.toISOString(), + parseDate: (s) => new Date(s) + } + + // currency.js + const currencyModule = { + formatCurrency: (n) => `$${n.toFixed(2)}` + } + + // utils/index.js (barrel file) + // export { formatDate, parseDate } from './date.js' + // export { formatCurrency } from './currency.js' + const utilsBarrel = { + ...dateModule, + ...currencyModule + } + + // Consumer imports from barrel + const { formatDate, formatCurrency } = utilsBarrel + + expect(formatCurrency(19.99)).toBe('$19.99') + expect(typeof formatDate(new Date())).toBe('string') + }) + + it('should support re-exporting default as named', () => { + // logger.js + // export default class Logger { } + const loggerModule = { + default: class Logger { + log(msg) { return msg } + } + } + + // utils/index.js + // export { default as Logger } from './logger.js' + const utilsBarrel = { + Logger: loggerModule.default + } + + const { Logger } = utilsBarrel + const logger = new Logger() + expect(logger.log('test')).toBe('test') + }) + + it('should support re-exporting all (export *)', () => { + // math.js exports multiple functions + const mathModule = { + add: (a, b) => a + b, + subtract: (a, b) => a - b, + multiply: (a, b) => a * b + } + + // utils/index.js + // export * from './math.js' + const utilsBarrel = { ...mathModule } + + expect(utilsBarrel.add(1, 2)).toBe(3) + expect(utilsBarrel.subtract(5, 3)).toBe(2) + expect(utilsBarrel.multiply(4, 5)).toBe(20) + }) + }) + }) + + // =========================================== + // Part 7: Module Characteristics + // =========================================== + + describe('Part 7: Module Characteristics', () => { + describe('Automatic Strict Mode', () => { + it('should demonstrate strict mode behaviors', () => { + // ES Modules are always in strict mode + + // Assigning to undeclared variable throws + expect(() => { + 'use strict' + undeclaredVar = 'oops' + }).toThrow(ReferenceError) + }) + + it('should prevent duplicate parameters in strict mode', () => { + // In strict mode, duplicate parameter names are syntax errors + // This would be caught at parse time in a real module: + // function f(a, a) { } // SyntaxError + + // We can test that strict mode is enforced + expect(() => { + 'use strict' + eval('function f(a, a) {}') + }).toThrow(SyntaxError) + }) + + it('should make this undefined in functions called without context', () => { + 'use strict' + + function getThis() { + return this + } + + expect(getThis()).toBe(undefined) + }) + }) + + describe('Module Scope (not global)', () => { + it('should keep module variables private by default', () => { + // In a module, top-level variables are scoped to the module + const createModule = () => { + const privateValue = 'secret' + const publicValue = 'visible' + + return { + publicValue, + getPrivate: () => privateValue + } + } + + const mod = createModule() + + expect(mod.publicValue).toBe('visible') + expect(mod.getPrivate()).toBe('secret') + expect(mod.privateValue).toBe(undefined) // Not exposed + }) + + it('should not leak var to global scope in modules', () => { + // In regular scripts, var leaks to window + // In modules, var is module-scoped + + const createModule = () => { + var moduleVar = 'module scoped' + return { getVar: () => moduleVar } + } + + const mod = createModule() + expect(mod.getVar()).toBe('module scoped') + expect(typeof moduleVar).toBe('undefined') // Not in outer scope + }) + }) + + describe('Top-level this is undefined', () => { + it('should have undefined this at module top level', () => { + // In ES Modules, top-level this is undefined + // (not window or global) + + // Regular function in strict mode has undefined this when called without context + function getThisInStrictMode() { + 'use strict' + return this + } + + // Called without context, this is undefined (like module top-level) + expect(getThisInStrictMode()).toBe(undefined) + + // Arrow functions capture this from enclosing scope + // In a real ES module, this would be undefined at the top level + const arrowThis = (() => this)() + + // Note: In test environment, the outer `this` may not be undefined + // but in a real ES module file, top-level `this` IS undefined + // This test demonstrates the concept via strict mode function + }) + }) + + describe('Import Hoisting', () => { + it('should demonstrate that imports are hoisted', () => { + // In ES Modules, import declarations are hoisted + // The imported bindings are available throughout the module + + // This would work in a real module: + // console.log(helper()) // Works! Imports are hoisted + // import { helper } from './utils.js' + + // We simulate by showing the concept + const moduleCode = () => { + // Imports are processed first, before any code runs + const imports = { helper: () => 'helped' } + + // Then code runs, with imports already available + const result = imports.helper() // Can use before "import line" + return result + } + + expect(moduleCode()).toBe('helped') + }) + }) + }) + + // =========================================== + // Part 8: Common Mistakes + // =========================================== + + describe('Part 8: Common Mistakes', () => { + describe('Mistake #1: Named vs Default Import Confusion', () => { + it('should demonstrate the difference between named and default imports', () => { + const moduleWithBoth = { + default: function Logger() { return 'default' }, + format: () => 'named format' + } + + // CORRECT: No braces for default + // import Logger from './logger.js' + const Logger = moduleWithBoth.default + + // CORRECT: Braces for named + // import { format } from './logger.js' + const { format } = moduleWithBoth + + expect(Logger()).toBe('default') + expect(format()).toBe('named format') + + // WRONG would be: + // import { Logger } from './logger.js' // Error: no named export 'Logger' + expect(moduleWithBoth.Logger).toBe(undefined) // Not a named export! + }) + + it('should show the curly brace rule', () => { + /* + export default X → import X from '...' (no braces) + export { Y } → import { Y } from '...' (braces) + export { Z as W } → import { W } from '...' (braces) + */ + + const modA = { default: 'X value' } + const modB = { Y: 'Y value' } + const modC = { W: 'Z exported as W' } + + const X = modA.default // No braces + const { Y } = modB // Braces + const { W } = modC // Braces (renamed) + + expect(X).toBe('X value') + expect(Y).toBe('Y value') + expect(W).toBe('Z exported as W') + }) + }) + + describe('Mistake #2: Missing File Extensions', () => { + it('should demonstrate that extensions are required', () => { + // In browsers and Node.js ESM, file extensions are required + + const validPaths = [ + './utils.js', // Correct + './components/Button.js', // Correct + '../helpers.mjs', // Correct + ] + + const invalidPaths = [ + './utils', // Missing extension - 404 in browser + './components/Button', // Missing extension - ERR_MODULE_NOT_FOUND + ] + + // All valid paths have extensions + validPaths.forEach(path => { + expect(path).toMatch(/\.(js|mjs|cjs)$/) + }) + + // Invalid paths lack extensions + invalidPaths.forEach(path => { + expect(path).not.toMatch(/\.(js|mjs|cjs)$/) + }) + }) + }) + + describe('Mistake #3: Using require in ESM', () => { + it('should show that require is not available in ESM', () => { + // In ESM files, require() is not defined + // This would throw: ReferenceError: require is not defined + + const esmEnvironment = { + require: undefined, // Not available + import: () => Promise.resolve({}), // Use this instead + importMeta: { url: 'file:///path/to/module.js' } + } + + expect(esmEnvironment.require).toBe(undefined) + expect(typeof esmEnvironment.import).toBe('function') + }) + + it('should show createRequire workaround', () => { + // If you need require in ESM (for CommonJS packages): + // import { createRequire } from 'module' + // const require = createRequire(import.meta.url) + + // Simulating createRequire + const createRequire = (url) => { + return (moduleName) => { + // This would actually load CommonJS modules + return { loaded: moduleName, from: url } + } + } + + const require = createRequire('file:///app/main.js') + const legacyModule = require('some-commonjs-package') + + expect(legacyModule.loaded).toBe('some-commonjs-package') + }) + }) + }) + + // =========================================== + // Part 9: Test Your Knowledge (from docs) + // =========================================== + + describe('Part 9: Test Your Knowledge', () => { + describe('Q1: Static vs Dynamic - Why tree-shaking works', () => { + it('should show ESM imports are statically analyzable', () => { + // ESM imports are declarations, not function calls + // Bundlers can see exactly what's imported without running code + + const moduleExports = { + add: (a, b) => a + b, + subtract: (a, b) => a - b, + multiply: (a, b) => a * b, + divide: (a, b) => a / b + } + + // Static import - bundler knows only 'add' is used + const { add } = moduleExports + + // The other functions can be tree-shaken out + const usedExports = ['add'] + const unusedExports = ['subtract', 'multiply', 'divide'] + + expect(usedExports).toContain('add') + expect(unusedExports).not.toContain('add') + }) + }) + + describe('Q2: Live bindings vs copies', () => { + it('should demonstrate the key difference', () => { + // ESM: live binding (reference) + const esmModule = { count: 0, increment() { this.count++ } } + + expect(esmModule.count).toBe(0) + esmModule.increment() + expect(esmModule.count).toBe(1) // Live - sees the change + + // CommonJS simulation: value copy + let cjsCount = 0 + const cjsExport = { + count: cjsCount, // Copy at export time + increment() { cjsCount++ } + } + + expect(cjsExport.count).toBe(0) + cjsExport.increment() + expect(cjsExport.count).toBe(0) // Still 0 - it's a copy + }) + }) + + describe('Q3: When to use dynamic imports', () => { + it('should use dynamic imports for conditional loading', async () => { + const features = { + charts: { render: () => 'chart' }, + maps: { render: () => 'map' } + } + + async function loadFeature(name) { + // Only loads when called, not at module load time + return Promise.resolve(features[name]) + } + + // Feature loaded on demand + const charts = await loadFeature('charts') + expect(charts.render()).toBe('chart') + }) + }) + + describe('Q4: Why extensions are required', () => { + it('should explain browser vs Node resolution', () => { + // Browsers make HTTP requests - can't try multiple extensions + // Node ESM matches browser behavior for consistency + + const browserRequest = (path) => { + // Browser requests exactly what you ask for + // Check for common JS extensions + const hasExtension = /\.(js|mjs|cjs|json)$/.test(path) + if (!hasExtension) { + return { status: 404, error: 'Not Found' } + } + return { status: 200, content: 'module code' } + } + + expect(browserRequest('./utils').status).toBe(404) + expect(browserRequest('./utils.js').status).toBe(200) + expect(browserRequest('./module.mjs').status).toBe(200) + }) + }) + + describe('Q5: What happens with circular dependencies', () => { + it('should show TDZ error with const/let', () => { + // With const/let, accessing before init throws ReferenceError + expect(() => { + const fn = () => { + console.log(x) // TDZ + const x = 1 + } + fn() + }).toThrow(ReferenceError) + }) + + it('should show deferred access pattern works', () => { + const moduleA = { value: null } + const moduleB = { + getValue: () => moduleA.value // Deferred - reads at call time + } + + expect(moduleB.getValue()).toBe(null) // A not initialized + + moduleA.value = 'initialized' + expect(moduleB.getValue()).toBe('initialized') // Works now + }) + }) + }) +}) diff --git a/tests/advanced-topics/modern-js-syntax/modern-js-syntax.test.js b/tests/advanced-topics/modern-js-syntax/modern-js-syntax.test.js new file mode 100644 index 00000000..db8b6f79 --- /dev/null +++ b/tests/advanced-topics/modern-js-syntax/modern-js-syntax.test.js @@ -0,0 +1,646 @@ +import { describe, it, expect } from 'vitest' + +describe('Modern JavaScript Syntax (ES6+)', () => { + + // =========================================== + // ARROW FUNCTIONS + // =========================================== + describe('Arrow Functions', () => { + it('should have concise syntax for single expressions', () => { + const add = (a, b) => a + b + const square = x => x * x + const greet = () => 'Hello!' + + expect(add(2, 3)).toBe(5) + expect(square(4)).toBe(16) + expect(greet()).toBe('Hello!') + }) + + it('should require explicit return in block body', () => { + const withBlock = (a, b) => { return a + b } + const withoutReturn = (a, b) => { a + b } // Returns undefined + + expect(withBlock(2, 3)).toBe(5) + expect(withoutReturn(2, 3)).toBe(undefined) + }) + + it('should require parentheses when returning object literal', () => { + // Without parentheses, braces are interpreted as function body (labeled statement) + const wrong = name => { name: name } // This is a labeled statement, returns undefined + const correct = name => ({ name: name }) // Parentheses make it an object literal + + expect(wrong('Alice')).toBe(undefined) + expect(correct('Alice')).toEqual({ name: 'Alice' }) + + // Note: Adding a comma like { name: name, active: true } would be a SyntaxError + }) + + it('should inherit this from enclosing scope', () => { + const obj = { + value: 42, + getValueArrow: function() { + const arrow = () => this.value + return arrow() + }, + getValueRegular: function() { + // In strict mode, 'this' inside a plain function call is undefined + // This would throw an error if we try to access this.value + const regular = function() { return this } + return regular() + } + } + + expect(obj.getValueArrow()).toBe(42) + // Arrow function correctly inherits 'this' from getValueArrow + // Regular function loses 'this' binding (undefined in strict mode) + expect(obj.getValueRegular()).toBe(undefined) + }) + }) + + // =========================================== + // DESTRUCTURING + // =========================================== + describe('Destructuring', () => { + describe('Array Destructuring', () => { + it('should extract values by position', () => { + const colors = ['red', 'green', 'blue'] + const [first, second, third] = colors + + expect(first).toBe('red') + expect(second).toBe('green') + expect(third).toBe('blue') + }) + + it('should skip elements with empty slots', () => { + const numbers = [1, 2, 3, 4, 5] + const [first, , third, , fifth] = numbers + + expect(first).toBe(1) + expect(third).toBe(3) + expect(fifth).toBe(5) + }) + + it('should support default values', () => { + const [a, b, c = 'default'] = [1, 2] + + expect(a).toBe(1) + expect(b).toBe(2) + expect(c).toBe('default') + }) + + it('should support rest pattern', () => { + const [head, ...tail] = [1, 2, 3, 4, 5] + + expect(head).toBe(1) + expect(tail).toEqual([2, 3, 4, 5]) + }) + + it('should swap variables without temp', () => { + let x = 1 + let y = 2 + + ;[x, y] = [y, x] + + expect(x).toBe(2) + expect(y).toBe(1) + }) + }) + + describe('Object Destructuring', () => { + it('should extract properties by name', () => { + const user = { name: 'Alice', age: 25 } + const { name, age } = user + + expect(name).toBe('Alice') + expect(age).toBe(25) + }) + + it('should support renaming', () => { + const user = { name: 'Alice', age: 25 } + const { name: userName, age: userAge } = user + + expect(userName).toBe('Alice') + expect(userAge).toBe(25) + }) + + it('should support default values', () => { + const user = { name: 'Alice' } + const { name, role = 'guest' } = user + + expect(name).toBe('Alice') + expect(role).toBe('guest') + }) + + it('should support nested destructuring', () => { + const user = { + name: 'Alice', + address: { city: 'Portland', country: 'USA' } + } + const { address: { city } } = user + + expect(city).toBe('Portland') + }) + + it('should support rest pattern', () => { + const user = { id: 1, name: 'Alice', age: 25 } + const { id, ...rest } = user + + expect(id).toBe(1) + expect(rest).toEqual({ name: 'Alice', age: 25 }) + }) + }) + + describe('Function Parameter Destructuring', () => { + it('should destructure parameters', () => { + function greet({ name, greeting = 'Hello' }) { + return `${greeting}, ${name}!` + } + + expect(greet({ name: 'Alice' })).toBe('Hello, Alice!') + expect(greet({ name: 'Bob', greeting: 'Hi' })).toBe('Hi, Bob!') + }) + + it('should handle empty parameter with default', () => { + function greet({ name = 'Guest' } = {}) { + return `Hello, ${name}!` + } + + expect(greet()).toBe('Hello, Guest!') + expect(greet({})).toBe('Hello, Guest!') + expect(greet({ name: 'Alice' })).toBe('Hello, Alice!') + }) + }) + }) + + // =========================================== + // SPREAD AND REST OPERATORS + // =========================================== + describe('Spread and Rest Operators', () => { + describe('Spread Operator', () => { + it('should spread arrays', () => { + const arr1 = [1, 2, 3] + const arr2 = [4, 5, 6] + + expect([...arr1, ...arr2]).toEqual([1, 2, 3, 4, 5, 6]) + expect([0, ...arr1, 4]).toEqual([0, 1, 2, 3, 4]) + }) + + it('should copy arrays (shallow)', () => { + const original = [1, 2, 3] + const copy = [...original] + + expect(copy).toEqual(original) + expect(copy).not.toBe(original) + }) + + it('should spread objects', () => { + const defaults = { theme: 'light', fontSize: 14 } + const userPrefs = { theme: 'dark' } + + const merged = { ...defaults, ...userPrefs } + + expect(merged).toEqual({ theme: 'dark', fontSize: 14 }) + }) + + it('should spread function arguments', () => { + const numbers = [1, 5, 3, 9, 2] + + expect(Math.max(...numbers)).toBe(9) + expect(Math.min(...numbers)).toBe(1) + }) + + it('should create shallow copies only', () => { + const original = { nested: { value: 1 } } + const copy = { ...original } + + copy.nested.value = 2 + + // Both are affected because nested object is shared + expect(original.nested.value).toBe(2) + expect(copy.nested.value).toBe(2) + }) + }) + + describe('Rest Parameters', () => { + it('should collect remaining arguments', () => { + function sum(...numbers) { + return numbers.reduce((total, n) => total + n, 0) + } + + expect(sum(1, 2, 3)).toBe(6) + expect(sum(1, 2, 3, 4, 5)).toBe(15) + expect(sum()).toBe(0) + }) + + it('should work with named parameters', () => { + function log(first, ...rest) { + return { first, rest } + } + + expect(log('a', 'b', 'c', 'd')).toEqual({ + first: 'a', + rest: ['b', 'c', 'd'] + }) + }) + }) + }) + + // =========================================== + // TEMPLATE LITERALS + // =========================================== + describe('Template Literals', () => { + it('should interpolate expressions', () => { + const name = 'Alice' + const age = 25 + + expect(`Hello, ${name}!`).toBe('Hello, Alice!') + expect(`Age: ${age}`).toBe('Age: 25') + expect(`Next year: ${age + 1}`).toBe('Next year: 26') + }) + + it('should support multi-line strings', () => { + const multiLine = `line 1 +line 2 +line 3` + + expect(multiLine).toContain('line 1') + expect(multiLine).toContain('\n') + expect(multiLine.split('\n').length).toBe(3) + }) + + it('should work with tagged templates', () => { + function upper(strings, ...values) { + return strings.reduce((result, str, i) => { + const value = values[i] ? values[i].toString().toUpperCase() : '' + return result + str + value + }, '') + } + + const name = 'alice' + expect(upper`Hello, ${name}!`).toBe('Hello, ALICE!') + }) + }) + + // =========================================== + // OPTIONAL CHAINING + // =========================================== + describe('Optional Chaining', () => { + it('should safely access nested properties', () => { + const user = { name: 'Alice' } + const userWithAddress = { name: 'Bob', address: { city: 'Portland' } } + + expect(user?.address?.city).toBe(undefined) + expect(userWithAddress?.address?.city).toBe('Portland') + }) + + it('should short-circuit on null/undefined', () => { + const user = null + + expect(user?.name).toBe(undefined) + expect(user?.address?.city).toBe(undefined) + }) + + it('should work with bracket notation', () => { + const user = { profile: { name: 'Alice' } } + const prop = 'profile' + + expect(user?.[prop]?.name).toBe('Alice') + expect(user?.['nonexistent']?.name).toBe(undefined) + }) + + it('should work with function calls', () => { + const obj = { + greet: () => 'Hello!' + } + + expect(obj.greet?.()).toBe('Hello!') + expect(obj.nonexistent?.()).toBe(undefined) + }) + }) + + // =========================================== + // NULLISH COALESCING + // =========================================== + describe('Nullish Coalescing', () => { + it('should return right side only for null/undefined', () => { + expect(null ?? 'default').toBe('default') + expect(undefined ?? 'default').toBe('default') + expect(0 ?? 'default').toBe(0) + expect('' ?? 'default').toBe('') + expect(false ?? 'default').toBe(false) + expect(NaN ?? 'default').toBeNaN() + }) + + it('should differ from logical OR', () => { + // || returns right side for any falsy value + expect(0 || 'default').toBe('default') + expect('' || 'default').toBe('default') + expect(false || 'default').toBe('default') + + // ?? only returns right side for null/undefined + expect(0 ?? 'default').toBe(0) + expect('' ?? 'default').toBe('') + expect(false ?? 'default').toBe(false) + }) + + it('should combine with optional chaining', () => { + const user = null + + expect(user?.name ?? 'Anonymous').toBe('Anonymous') + + const userWithName = { name: 'Alice' } + expect(userWithName?.name ?? 'Anonymous').toBe('Alice') + }) + }) + + // =========================================== + // LOGICAL ASSIGNMENT OPERATORS + // =========================================== + describe('Logical Assignment Operators', () => { + it('should support nullish coalescing assignment (??=)', () => { + let a = null + let b = 'value' + let c = 0 + + a ??= 'default' + b ??= 'default' + c ??= 'default' + + expect(a).toBe('default') + expect(b).toBe('value') + expect(c).toBe(0) + }) + + it('should support logical OR assignment (||=)', () => { + let a = null + let b = 'value' + let c = 0 + + a ||= 'default' + b ||= 'default' + c ||= 'default' + + expect(a).toBe('default') + expect(b).toBe('value') + expect(c).toBe('default') // 0 is falsy + }) + + it('should support logical AND assignment (&&=)', () => { + let a = null + let b = 'value' + + a &&= 'updated' + b &&= 'updated' + + expect(a).toBe(null) // null is falsy, so no assignment + expect(b).toBe('updated') // 'value' is truthy, so assign + }) + }) + + // =========================================== + // DEFAULT PARAMETERS + // =========================================== + describe('Default Parameters', () => { + it('should provide default values', () => { + function greet(name = 'Guest', greeting = 'Hello') { + return `${greeting}, ${name}!` + } + + expect(greet()).toBe('Hello, Guest!') + expect(greet('Alice')).toBe('Hello, Alice!') + expect(greet('Bob', 'Hi')).toBe('Hi, Bob!') + }) + + it('should only trigger on undefined, not null', () => { + function example(value = 'default') { + return value + } + + expect(example(undefined)).toBe('default') + expect(example(null)).toBe(null) + expect(example(0)).toBe(0) + expect(example('')).toBe('') + expect(example(false)).toBe(false) + }) + + it('should allow earlier parameters as defaults', () => { + function createRect(width, height = width) { + return { width, height } + } + + expect(createRect(10)).toEqual({ width: 10, height: 10 }) + expect(createRect(10, 20)).toEqual({ width: 10, height: 20 }) + }) + + it('should evaluate default expressions each time', () => { + let counter = 0 + function getDefault() { return ++counter } + + function example(value = getDefault()) { + return value + } + + expect(example()).toBe(1) + expect(example()).toBe(2) + expect(example()).toBe(3) + expect(example(100)).toBe(100) // getDefault not called + expect(example()).toBe(4) + }) + }) + + // =========================================== + // ENHANCED OBJECT LITERALS + // =========================================== + describe('Enhanced Object Literals', () => { + it('should support property shorthand', () => { + const name = 'Alice' + const age = 25 + + const user = { name, age } + + expect(user).toEqual({ name: 'Alice', age: 25 }) + }) + + it('should support method shorthand', () => { + const calculator = { + add(a, b) { return a + b }, + subtract(a, b) { return a - b } + } + + expect(calculator.add(5, 3)).toBe(8) + expect(calculator.subtract(5, 3)).toBe(2) + }) + + it('should support computed property names', () => { + const key = 'dynamicKey' + const index = 0 + + const obj = { + [key]: 'value', + [`item_${index}`]: 'first' + } + + expect(obj.dynamicKey).toBe('value') + expect(obj.item_0).toBe('first') + }) + }) + + // =========================================== + // MAP, SET, AND SYMBOL + // =========================================== + describe('Map', () => { + it('should store key-value pairs with any key type', () => { + const map = new Map() + const objKey = { id: 1 } + + map.set('string', 'value1') + map.set(42, 'value2') + map.set(objKey, 'value3') + + expect(map.get('string')).toBe('value1') + expect(map.get(42)).toBe('value2') + expect(map.get(objKey)).toBe('value3') + expect(map.size).toBe(3) + }) + + it('should maintain insertion order', () => { + const map = new Map([['c', 3], ['a', 1], ['b', 2]]) + const keys = [...map.keys()] + + expect(keys).toEqual(['c', 'a', 'b']) + }) + + it('should be iterable', () => { + const map = new Map([['a', 1], ['b', 2]]) + const entries = [] + + for (const [key, value] of map) { + entries.push([key, value]) + } + + expect(entries).toEqual([['a', 1], ['b', 2]]) + }) + }) + + describe('Set', () => { + it('should store unique values', () => { + const set = new Set([1, 2, 2, 3, 3, 3]) + + expect(set.size).toBe(3) + expect([...set]).toEqual([1, 2, 3]) + }) + + it('should remove duplicates from arrays', () => { + const numbers = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4] + const unique = [...new Set(numbers)] + + expect(unique).toEqual([1, 2, 3, 4]) + }) + + it('should support set operations', () => { + const a = new Set([1, 2, 3]) + const b = new Set([2, 3, 4]) + + const union = new Set([...a, ...b]) + const intersection = [...a].filter(x => b.has(x)) + const difference = [...a].filter(x => !b.has(x)) + + expect([...union]).toEqual([1, 2, 3, 4]) + expect(intersection).toEqual([2, 3]) + expect(difference).toEqual([1]) + }) + }) + + describe('Symbol', () => { + it('should create unique values', () => { + const sym1 = Symbol('description') + const sym2 = Symbol('description') + + expect(sym1).not.toBe(sym2) + }) + + it('should work as object keys', () => { + const ID = Symbol('id') + const user = { + name: 'Alice', + [ID]: 12345 + } + + expect(user[ID]).toBe(12345) + expect(Object.keys(user)).toEqual(['name']) // Symbol not included + }) + + it('should support global registry with Symbol.for', () => { + const sym1 = Symbol.for('shared') + const sym2 = Symbol.for('shared') + + expect(sym1).toBe(sym2) + expect(Symbol.keyFor(sym1)).toBe('shared') + }) + }) + + // =========================================== + // FOR...OF LOOP + // =========================================== + describe('for...of Loop', () => { + it('should iterate over array values', () => { + const arr = ['a', 'b', 'c'] + const values = [] + + for (const value of arr) { + values.push(value) + } + + expect(values).toEqual(['a', 'b', 'c']) + }) + + it('should iterate over string characters', () => { + const chars = [] + + for (const char of 'hello') { + chars.push(char) + } + + expect(chars).toEqual(['h', 'e', 'l', 'l', 'o']) + }) + + it('should iterate over Map entries', () => { + const map = new Map([['a', 1], ['b', 2]]) + const entries = [] + + for (const [key, value] of map) { + entries.push({ key, value }) + } + + expect(entries).toEqual([ + { key: 'a', value: 1 }, + { key: 'b', value: 2 } + ]) + }) + + it('should iterate over Set values', () => { + const set = new Set([1, 2, 3]) + const values = [] + + for (const value of set) { + values.push(value) + } + + expect(values).toEqual([1, 2, 3]) + }) + + it('should work with destructuring', () => { + const users = [ + { name: 'Alice', age: 25 }, + { name: 'Bob', age: 30 } + ] + const names = [] + + for (const { name } of users) { + names.push(name) + } + + expect(names).toEqual(['Alice', 'Bob']) + }) + }) +}) diff --git a/tests/advanced-topics/regular-expressions/regular-expressions.test.js b/tests/advanced-topics/regular-expressions/regular-expressions.test.js new file mode 100644 index 00000000..9e291a61 --- /dev/null +++ b/tests/advanced-topics/regular-expressions/regular-expressions.test.js @@ -0,0 +1,366 @@ +import { describe, it, expect } from 'vitest' + +describe('Regular Expressions', () => { + describe('Creating Regex', () => { + it('should create regex with literal syntax', () => { + const pattern = /hello/ + expect(pattern.test('hello world')).toBe(true) + expect(pattern.test('world')).toBe(false) + }) + + it('should create regex with RegExp constructor', () => { + const pattern = new RegExp('hello') + expect(pattern.test('hello world')).toBe(true) + expect(pattern.test('world')).toBe(false) + }) + + it('should create dynamic patterns with RegExp constructor', () => { + const searchTerm = 'cat' + const pattern = new RegExp(searchTerm, 'gi') + expect('Cat CAT cat'.match(pattern)).toEqual(['Cat', 'CAT', 'cat']) + }) + }) + + describe('Character Classes', () => { + it('should match digits with \\d', () => { + const pattern = /\d+/ + expect(pattern.test('123')).toBe(true) + expect(pattern.test('abc')).toBe(false) + expect('abc123def'.match(pattern)[0]).toBe('123') + }) + + it('should match word characters with \\w', () => { + const pattern = /\w+/g + expect('hello_world 123'.match(pattern)).toEqual(['hello_world', '123']) + }) + + it('should match whitespace with \\s', () => { + const pattern = /\s+/ + expect(pattern.test('hello world')).toBe(true) + expect(pattern.test('helloworld')).toBe(false) + }) + + it('should match any character with .', () => { + const pattern = /a.c/ + expect(pattern.test('abc')).toBe(true) + expect(pattern.test('a1c')).toBe(true) + expect(pattern.test('ac')).toBe(false) + }) + + it('should match character sets with []', () => { + const vowelPattern = /[aeiou]/g + expect('hello'.match(vowelPattern)).toEqual(['e', 'o']) + }) + + it('should match negated character sets with [^]', () => { + const nonDigitPattern = /[^0-9]+/g + expect('abc123def'.match(nonDigitPattern)).toEqual(['abc', 'def']) + }) + + it('should match character ranges', () => { + const lowercasePattern = /[a-z]+/g + expect('Hello World'.match(lowercasePattern)).toEqual(['ello', 'orld']) + }) + }) + + describe('Quantifiers', () => { + it('should match 0 or more with *', () => { + const pattern = /ab*c/ + expect(pattern.test('ac')).toBe(true) + expect(pattern.test('abc')).toBe(true) + expect(pattern.test('abbbbc')).toBe(true) + }) + + it('should match 1 or more with +', () => { + const pattern = /ab+c/ + expect(pattern.test('ac')).toBe(false) + expect(pattern.test('abc')).toBe(true) + expect(pattern.test('abbbbc')).toBe(true) + }) + + it('should match 0 or 1 with ?', () => { + const pattern = /colou?r/ + expect(pattern.test('color')).toBe(true) + expect(pattern.test('colour')).toBe(true) + expect(pattern.test('colouur')).toBe(false) + }) + + it('should match exact count with {n}', () => { + const pattern = /\d{4}/ + expect(pattern.test('2024')).toBe(true) + expect(pattern.test('123')).toBe(false) + }) + + it('should match range with {n,m}', () => { + const pattern = /\d{2,4}/ + expect('1'.match(pattern)).toBe(null) + expect('12'.match(pattern)[0]).toBe('12') + expect('12345'.match(pattern)[0]).toBe('1234') + }) + + it('should match n or more with {n,}', () => { + const pattern = /\d{2,}/ + expect(pattern.test('1')).toBe(false) + expect(pattern.test('12')).toBe(true) + expect(pattern.test('12345')).toBe(true) + }) + }) + + describe('Anchors', () => { + it('should match start of string with ^', () => { + const pattern = /^Hello/ + expect(pattern.test('Hello World')).toBe(true) + expect(pattern.test('Say Hello')).toBe(false) + }) + + it('should match end of string with $', () => { + const pattern = /World$/ + expect(pattern.test('Hello World')).toBe(true) + expect(pattern.test('World Hello')).toBe(false) + }) + + it('should match entire string with ^ and $', () => { + const pattern = /^\d+$/ + expect(pattern.test('12345')).toBe(true) + expect(pattern.test('123abc')).toBe(false) + expect(pattern.test('abc123')).toBe(false) + }) + + it('should match word boundaries with \\b', () => { + const pattern = /\bcat\b/ + expect(pattern.test('cat')).toBe(true) + expect(pattern.test('the cat sat')).toBe(true) + expect(pattern.test('category')).toBe(false) + expect(pattern.test('concatenate')).toBe(false) + }) + }) + + describe('Methods', () => { + describe('test()', () => { + it('should return true for matches', () => { + expect(/\d+/.test('123')).toBe(true) + }) + + it('should return false for non-matches', () => { + expect(/\d+/.test('abc')).toBe(false) + }) + }) + + describe('match()', () => { + it('should return first match without g flag', () => { + const result = 'cat and cat'.match(/cat/) + expect(result[0]).toBe('cat') + expect(result.index).toBe(0) + }) + + it('should return all matches with g flag', () => { + const result = 'cat and cat'.match(/cat/g) + expect(result).toEqual(['cat', 'cat']) + }) + + it('should return null when no match', () => { + expect('hello'.match(/\d+/)).toBe(null) + }) + }) + + describe('replace()', () => { + it('should replace first match without g flag', () => { + expect('hello world'.replace(/o/, '0')).toBe('hell0 world') + }) + + it('should replace all matches with g flag', () => { + expect('hello world'.replace(/o/g, '0')).toBe('hell0 w0rld') + }) + + it('should use captured groups in replacement', () => { + expect('John Smith'.replace(/(\w+) (\w+)/, '$2, $1')).toBe('Smith, John') + }) + }) + + describe('split()', () => { + it('should split by regex pattern', () => { + expect('a, b, c'.split(/,\s*/)).toEqual(['a', 'b', 'c']) + }) + + it('should split on whitespace', () => { + expect('hello world foo'.split(/\s+/)).toEqual(['hello', 'world', 'foo']) + }) + }) + + describe('exec()', () => { + it('should return match with details', () => { + const result = /\d+/.exec('abc123def') + expect(result[0]).toBe('123') + expect(result.index).toBe(3) + }) + + it('should return null for no match', () => { + expect(/\d+/.exec('abc')).toBe(null) + }) + }) + }) + + describe('Flags', () => { + it('should match case-insensitively with i flag', () => { + const pattern = /hello/i + expect(pattern.test('HELLO')).toBe(true) + expect(pattern.test('Hello')).toBe(true) + expect(pattern.test('hello')).toBe(true) + }) + + it('should find all matches with g flag', () => { + const pattern = /a/g + expect('banana'.match(pattern)).toEqual(['a', 'a', 'a']) + }) + + it('should match line boundaries with m flag', () => { + const text = 'line1\nline2\nline3' + const pattern = /^line\d/gm + expect(text.match(pattern)).toEqual(['line1', 'line2', 'line3']) + }) + + it('should combine multiple flags', () => { + const pattern = /hello/gi + expect('Hello HELLO hello'.match(pattern)).toEqual(['Hello', 'HELLO', 'hello']) + }) + }) + + describe('Capturing Groups', () => { + it('should capture groups with parentheses', () => { + const pattern = /(\d{3})-(\d{4})/ + const match = '555-1234'.match(pattern) + expect(match[0]).toBe('555-1234') + expect(match[1]).toBe('555') + expect(match[2]).toBe('1234') + }) + + it('should support named groups', () => { + const pattern = /(?<area>\d{3})-(?<number>\d{4})/ + const match = '555-1234'.match(pattern) + expect(match.groups.area).toBe('555') + expect(match.groups.number).toBe('1234') + }) + + it('should use groups in replace with $n', () => { + const result = '12-25-2024'.replace( + /(\d{2})-(\d{2})-(\d{4})/, + '$3/$1/$2' + ) + expect(result).toBe('2024/12/25') + }) + + it('should use named groups in replace', () => { + const result = '12-25-2024'.replace( + /(?<month>\d{2})-(?<day>\d{2})-(?<year>\d{4})/, + '$<year>/$<month>/$<day>' + ) + expect(result).toBe('2024/12/25') + }) + + it('should support non-capturing groups with (?:)', () => { + const pattern = /(?:ab)+/ + const match = 'ababab'.match(pattern) + expect(match[0]).toBe('ababab') + expect(match[1]).toBeUndefined() + }) + }) + + describe('Greedy vs Lazy', () => { + it('should match greedily by default', () => { + const html = '<div>Hello</div><div>World</div>' + const greedy = /<div>.*<\/div>/ + expect(html.match(greedy)[0]).toBe('<div>Hello</div><div>World</div>') + }) + + it('should match lazily with ?', () => { + const html = '<div>Hello</div><div>World</div>' + const lazy = /<div>.*?<\/div>/ + expect(html.match(lazy)[0]).toBe('<div>Hello</div>') + }) + + it('should find all lazy matches with g flag', () => { + const html = '<div>Hello</div><div>World</div>' + const lazy = /<div>.*?<\/div>/g + expect(html.match(lazy)).toEqual(['<div>Hello</div>', '<div>World</div>']) + }) + }) + + describe('Common Patterns', () => { + it('should validate basic email format', () => { + const email = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + expect(email.test('user@example.com')).toBe(true) + expect(email.test('user.name@example.co.uk')).toBe(true) + expect(email.test('invalid-email')).toBe(false) + expect(email.test('missing@domain')).toBe(false) + expect(email.test('@missing-local.com')).toBe(false) + }) + + it('should validate URL format', () => { + const url = /^https?:\/\/[^\s]+$/ + expect(url.test('https://example.com')).toBe(true) + expect(url.test('http://example.com/path')).toBe(true) + expect(url.test('ftp://example.com')).toBe(false) + expect(url.test('not a url')).toBe(false) + }) + + it('should validate US phone number formats', () => { + const phone = /^(\(\d{3}\)|\d{3})[-.\s]?\d{3}[-.\s]?\d{4}$/ + expect(phone.test('555-123-4567')).toBe(true) + expect(phone.test('(555) 123-4567')).toBe(true) + expect(phone.test('555.123.4567')).toBe(true) + expect(phone.test('5551234567')).toBe(true) + expect(phone.test('55-123-4567')).toBe(false) + }) + + it('should validate username format', () => { + const username = /^[a-zA-Z0-9_]{3,16}$/ + expect(username.test('john_doe')).toBe(true) + expect(username.test('user123')).toBe(true) + expect(username.test('ab')).toBe(false) // too short + expect(username.test('this_is_way_too_long_username')).toBe(false) // too long + expect(username.test('invalid-user')).toBe(false) // hyphen not allowed + }) + + it('should extract hashtags from text', () => { + const hashtags = /#\w+/g + const text = 'Learning #JavaScript and #regex is fun! #coding' + expect(text.match(hashtags)).toEqual(['#JavaScript', '#regex', '#coding']) + }) + + it('should extract numbers from text', () => { + const numbers = /\d+/g + const text = 'I have 42 apples and 7 oranges' + expect(text.match(numbers)).toEqual(['42', '7']) + }) + }) + + describe('Edge Cases', () => { + it('should escape special characters in RegExp constructor', () => { + const searchTerm = 'hello.world' + const escaped = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + const pattern = new RegExp(escaped) + expect(pattern.test('hello.world')).toBe(true) + expect(pattern.test('helloXworld')).toBe(false) + }) + + it('should handle empty strings', () => { + expect(/.*/.test('')).toBe(true) + expect(/.+/.test('')).toBe(false) + const emptyMatch = ''.match(/\d*/) + expect(emptyMatch[0]).toBe('') + }) + + it('should handle alternation with |', () => { + const pattern = /cat|dog|bird/ + expect(pattern.test('I have a cat')).toBe(true) + expect(pattern.test('I have a dog')).toBe(true) + expect(pattern.test('I have a fish')).toBe(false) + }) + + it('should handle backreferences', () => { + const pattern = /(\w+)\s+\1/ + expect(pattern.test('hello hello')).toBe(true) + expect(pattern.test('hello world')).toBe(false) + }) + }) +}) diff --git a/tests/async-javascript/callbacks/callbacks.dom.test.js b/tests/async-javascript/callbacks/callbacks.dom.test.js new file mode 100644 index 00000000..b841bee0 --- /dev/null +++ b/tests/async-javascript/callbacks/callbacks.dom.test.js @@ -0,0 +1,239 @@ +/** + * @vitest-environment jsdom + */ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' + +// ============================================================ +// DOM EVENT HANDLER CALLBACKS +// From callbacks.mdx lines 401-434 +// Pattern 1: Event Handlers +// ============================================================ + +describe('DOM Event Handler Callbacks', () => { + let button + + beforeEach(() => { + // Create a fresh button element for each test + button = document.createElement('button') + button.id = 'myButton' + document.body.appendChild(button) + }) + + afterEach(() => { + // Clean up + document.body.innerHTML = '' + }) + + // From lines 405-416: DOM events with addEventListener + it('should execute callback when button is clicked', () => { + const output = [] + + // DOM events + const button = document.getElementById('myButton') + + button.addEventListener('click', function handleClick(event) { + output.push('Button clicked!') + output.push(`Event type: ${event.type}`) // "click" + output.push(`Target id: ${event.target.id}`) // "myButton" + }) + + // The callback receives an Event object with details about what happened + + // Simulate click + button.click() + + expect(output).toEqual([ + 'Button clicked!', + 'Event type: click', + 'Target id: myButton' + ]) + }) + + // From lines 420-434: Named functions for reusability and removal + it('should use named functions for reusability', () => { + const output = [] + + function handleClick(event) { + output.push(`Clicked: ${event.target.id}`) + } + + function handleMouseOver(event) { + output.push(`Mouseover: ${event.target.id}`) + } + + button.addEventListener('click', handleClick) + button.addEventListener('mouseover', handleMouseOver) + + // Simulate events + button.click() + button.dispatchEvent(new MouseEvent('mouseover', { bubbles: true })) + + expect(output).toEqual([ + 'Clicked: myButton', + 'Mouseover: myButton' + ]) + }) + + it('should remove event listeners with removeEventListener', () => { + const output = [] + + function handleClick(event) { + output.push('Clicked!') + } + + button.addEventListener('click', handleClick) + + // First click - handler is attached + button.click() + expect(output).toEqual(['Clicked!']) + + // Later, you can remove them: + button.removeEventListener('click', handleClick) + + // Second click - handler is removed + button.click() + expect(output).toEqual(['Clicked!']) // Still just one, handler was removed + }) + + it('should demonstrate multiple event listeners on same element', () => { + const output = [] + + button.addEventListener('click', () => output.push('Handler 1')) + button.addEventListener('click', () => output.push('Handler 2')) + button.addEventListener('click', () => output.push('Handler 3')) + + button.click() + + // All handlers execute in order of registration + expect(output).toEqual(['Handler 1', 'Handler 2', 'Handler 3']) + }) + + it('should demonstrate event object properties in callback', () => { + const eventData = {} + + button.addEventListener('click', function(event) { + eventData.type = event.type + eventData.target = event.target + eventData.currentTarget = event.currentTarget + eventData.bubbles = event.bubbles + eventData.cancelable = event.cancelable + }) + + button.click() + + expect(eventData.type).toBe('click') + expect(eventData.target).toBe(button) + expect(eventData.currentTarget).toBe(button) + expect(eventData.bubbles).toBe(true) + expect(eventData.cancelable).toBe(true) + }) + + it('should demonstrate event delegation pattern with callbacks', () => { + // Create a list with items + const list = document.createElement('ul') + list.id = 'myList' + + const item1 = document.createElement('li') + item1.textContent = 'Item 1' + item1.dataset.id = '1' + + const item2 = document.createElement('li') + item2.textContent = 'Item 2' + item2.dataset.id = '2' + + list.appendChild(item1) + list.appendChild(item2) + document.body.appendChild(list) + + const clickedItems = [] + + // Event delegation - single handler on parent + list.addEventListener('click', function(event) { + if (event.target.tagName === 'LI') { + clickedItems.push(event.target.dataset.id) + } + }) + + item1.click() + item2.click() + + expect(clickedItems).toEqual(['1', '2']) + }) + + it('should demonstrate this context in event handler callbacks', () => { + const results = [] + + // Regular function - 'this' is the element + button.addEventListener('click', function(event) { + results.push(`Regular: ${this.id}`) + }) + + // Arrow function - 'this' is NOT the element (inherited from outer scope) + button.addEventListener('click', (event) => { + // In this context, 'this' would be the module/global scope + results.push(`Arrow target: ${event.target.id}`) + }) + + button.click() + + expect(results).toEqual([ + 'Regular: myButton', + 'Arrow target: myButton' + ]) + }) + + it('should demonstrate once option for single-fire callbacks', () => { + const output = [] + + button.addEventListener('click', () => { + output.push('Clicked!') + }, { once: true }) + + button.click() + button.click() + button.click() + + // Handler only fires once + expect(output).toEqual(['Clicked!']) + }) + + it('should demonstrate preventing default with callbacks', () => { + const form = document.createElement('form') + const submitEvents = [] + let defaultPrevented = false + + form.addEventListener('submit', function(event) { + event.preventDefault() + defaultPrevented = event.defaultPrevented + submitEvents.push('Form submitted') + }) + + // Dispatch a submit event + const submitEvent = new Event('submit', { cancelable: true }) + form.dispatchEvent(submitEvent) + + expect(submitEvents).toEqual(['Form submitted']) + expect(defaultPrevented).toBe(true) + }) + + it('should demonstrate stopping propagation in callbacks', () => { + const output = [] + + // Create nested elements + const outer = document.createElement('div') + const inner = document.createElement('div') + outer.appendChild(inner) + document.body.appendChild(outer) + + outer.addEventListener('click', () => output.push('Outer clicked')) + inner.addEventListener('click', (event) => { + event.stopPropagation() + output.push('Inner clicked') + }) + + inner.click() + + // Only inner handler fires due to stopPropagation + expect(output).toEqual(['Inner clicked']) + }) +}) diff --git a/tests/async-javascript/callbacks/callbacks.test.js b/tests/async-javascript/callbacks/callbacks.test.js new file mode 100644 index 00000000..86e4c37d --- /dev/null +++ b/tests/async-javascript/callbacks/callbacks.test.js @@ -0,0 +1,1490 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' + +describe('Callbacks', () => { + + // ============================================================ + // OPENING EXAMPLES + // From callbacks.mdx lines 9-22, 139-155 + // ============================================================ + + describe('Opening Examples', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + // From lines 9-22: Why doesn't JavaScript wait? + it('should demonstrate setTimeout non-blocking behavior', async () => { + const output = [] + + output.push('Before timer') + + setTimeout(function() { + output.push('Timer fired!') + }, 1000) + + output.push('After timer') + + // Before timer advances, only sync code has run + expect(output).toEqual(['Before timer', 'After timer']) + + // After 1 second + await vi.advanceTimersByTimeAsync(1000) + + // Output: + // Before timer + // After timer + // Timer fired! (1 second later) + expect(output).toEqual(['Before timer', 'After timer', 'Timer fired!']) + }) + + // From lines 139-155: Restaurant buzzer analogy + it('should demonstrate restaurant buzzer analogy with eatBurger callback', async () => { + const output = [] + + // You place your order (start async operation) + setTimeout(function eatBurger() { + output.push('Eating my burger!') // This is the callback + }, 5000) + + // You go sit down (your code continues) + output.push('Sitting down, checking my phone...') + output.push('Chatting with friends...') + output.push('Reading the menu...') + + // Before timer fires + expect(output).toEqual([ + 'Sitting down, checking my phone...', + 'Chatting with friends...', + 'Reading the menu...' + ]) + + // After 5 seconds + await vi.advanceTimersByTimeAsync(5000) + + // Output: + // Sitting down, checking my phone... + // Chatting with friends... + // Reading the menu... + // Eating my burger! (5 seconds later) + expect(output).toEqual([ + 'Sitting down, checking my phone...', + 'Chatting with friends...', + 'Reading the menu...', + 'Eating my burger!' + ]) + }) + }) + + // ============================================================ + // WHAT IS A CALLBACK + // From callbacks.mdx lines 48-91 + // ============================================================ + + describe('What is a Callback', () => { + // From lines 48-61: greet and processUserInput example + it('should execute greet callback passed to processUserInput', () => { + const output = [] + + // greet is a callback function + function greet(name) { + output.push(`Hello, ${name}!`) + } + + // processUserInput accepts a callback + function processUserInput(callback) { + const name = 'Alice' + callback(name) // "calling back" the function we received + } + + processUserInput(greet) // "Hello, Alice!" + + expect(output).toEqual(['Hello, Alice!']) + }) + + // From lines 73-91: Callbacks can be anonymous + it('should work with anonymous function callbacks', () => { + const output = [] + + // Simulating addEventListener behavior for testing + function simulateAddEventListener(event, callback) { + callback() + } + + // Named function as callback + function handleClick() { + output.push('Clicked!') + } + simulateAddEventListener('click', handleClick) + + // Anonymous function as callback + simulateAddEventListener('click', function() { + output.push('Clicked!') + }) + + // Arrow function as callback + simulateAddEventListener('click', () => { + output.push('Clicked!') + }) + + // All three do the same thing + expect(output).toEqual(['Clicked!', 'Clicked!', 'Clicked!']) + }) + }) + + // ============================================================ + // CALLBACKS AND HIGHER-ORDER FUNCTIONS + // From callbacks.mdx lines 166-198 + // ============================================================ + + describe('Callbacks and Higher-Order Functions', () => { + // From lines 166-176: forEach is a higher-order function + it('should demonstrate forEach as a higher-order function', () => { + const output = [] + + // forEach is a HIGHER-ORDER FUNCTION (it accepts a function) + // The arrow function is the CALLBACK (it's being passed in) + + const numbers = [1, 2, 3] + + numbers.forEach((num) => { // <- This is the callback + output.push(num * 2) + }) + // 2, 4, 6 + + expect(output).toEqual([2, 4, 6]) + }) + + // From lines 180-198: filter, map, find, sort with users array + it('should demonstrate filter, map, find, sort with users array', () => { + const users = [ + { name: 'Alice', age: 25 }, + { name: 'Bob', age: 17 }, + { name: 'Charlie', age: 30 } + ] + + // filter accepts a callback that returns true/false + const adults = users.filter(user => user.age >= 18) + expect(adults).toEqual([ + { name: 'Alice', age: 25 }, + { name: 'Charlie', age: 30 } + ]) + + // map accepts a callback that transforms each element + const names = users.map(user => user.name) + expect(names).toEqual(['Alice', 'Bob', 'Charlie']) + + // find accepts a callback that returns true when found + const bob = users.find(user => user.name === 'Bob') + expect(bob).toEqual({ name: 'Bob', age: 17 }) + + // sort accepts a callback that compares two elements + const byAge = [...users].sort((a, b) => a.age - b.age) + expect(byAge).toEqual([ + { name: 'Bob', age: 17 }, + { name: 'Alice', age: 25 }, + { name: 'Charlie', age: 30 } + ]) + }) + }) + + // ============================================================ + // SYNCHRONOUS VS ASYNCHRONOUS CALLBACKS + // From callbacks.mdx lines 214-310 + // ============================================================ + + describe('Synchronous Callbacks', () => { + // From lines 214-236: Synchronous callbacks execute immediately + it('should execute map callbacks synchronously and in order', () => { + const output = [] + + const numbers = [1, 2, 3, 4, 5] + + output.push('Before map') + + const doubled = numbers.map(num => { + output.push(`Doubling ${num}`) + return num * 2 + }) + + output.push('After map') + output.push(JSON.stringify(doubled)) + + // Output (all synchronous, in order): + // Before map + // Doubling 1 + // Doubling 2 + // Doubling 3 + // Doubling 4 + // Doubling 5 + // After map + // [2, 4, 6, 8, 10] + expect(output).toEqual([ + 'Before map', + 'Doubling 1', + 'Doubling 2', + 'Doubling 3', + 'Doubling 4', + 'Doubling 5', + 'After map', + '[2,4,6,8,10]' + ]) + + expect(doubled).toEqual([2, 4, 6, 8, 10]) + }) + + // From lines 287-295: Synchronous callback - try/catch WORKS + it('should catch errors in synchronous callbacks with try/catch', () => { + let caughtMessage = null + + // Synchronous callback - try/catch WORKS + try { + [1, 2, 3].forEach(num => { + if (num === 2) throw new Error('Found 2!') + }) + } catch (error) { + caughtMessage = error.message // "Caught: Found 2!" + } + + expect(caughtMessage).toBe('Found 2!') + }) + }) + + describe('Asynchronous Callbacks', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + // From lines 249-262: Even with 0ms delay, callback runs after sync code + it('should execute setTimeout callbacks after synchronous code even with 0ms delay', async () => { + const output = [] + + output.push('Before setTimeout') + + setTimeout(() => { + output.push('Inside setTimeout') + }, 0) // Even with 0ms delay! + + output.push('After setTimeout') + + // Before timer fires + expect(output).toEqual(['Before setTimeout', 'After setTimeout']) + + await vi.advanceTimersByTimeAsync(0) + + // Output: + // Before setTimeout + // After setTimeout + // Inside setTimeout (runs AFTER all sync code) + expect(output).toEqual([ + 'Before setTimeout', + 'After setTimeout', + 'Inside setTimeout' + ]) + }) + + // From lines 297-306: Asynchronous callback - try/catch DOES NOT WORK! + it('should demonstrate that try/catch cannot catch async callback errors', async () => { + // This test verifies the concept that try/catch doesn't work for async callbacks + // In real code, the error would crash the program + + let tryCatchRan = false + const asyncCallback = vi.fn() + + // Asynchronous callback - try/catch DOES NOT WORK! + try { + setTimeout(() => { + asyncCallback() + // throw new Error('Async error!') // This error escapes! + }, 100) + } catch (error) { + // This will NEVER run + tryCatchRan = true + } + + // The try/catch completes immediately, before the callback even runs + expect(tryCatchRan).toBe(false) + expect(asyncCallback).not.toHaveBeenCalled() + + await vi.advanceTimersByTimeAsync(100) + + // Now the callback has run, but the try/catch is long gone + expect(asyncCallback).toHaveBeenCalled() + }) + }) + + // ============================================================ + // HOW CALLBACKS WORK WITH THE EVENT LOOP + // From callbacks.mdx lines 355-393 + // ============================================================ + + describe('Event Loop Examples', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + // From lines 355-387: Event loop trace example + it('should demonstrate event loop execution order', async () => { + const output = [] + + output.push('1: Script start') + + setTimeout(function first() { + output.push('2: First timeout') + }, 0) + + setTimeout(function second() { + output.push('3: Second timeout') + }, 0) + + output.push('4: Script end') + + // Execution order: + // 1. console.log('1: Script start') - runs immediately + // 2. setTimeout(first, 0) - registers first callback with Web APIs + // 3. setTimeout(second, 0) - registers second callback with Web APIs + // 4. console.log('4: Script end') - runs immediately + // 5. Call stack is now empty + // 6. Event Loop checks Task Queue - finds first + // 7. first() runs -> "2: First timeout" + // 8. Event Loop checks Task Queue - finds second + // 9. second() runs -> "3: Second timeout" + + // Before timers fire - only sync code has run + expect(output).toEqual(['1: Script start', '4: Script end']) + + await vi.advanceTimersByTimeAsync(0) + + // Output: + // 1: Script start + // 4: Script end + // 2: First timeout + // 3: Second timeout + expect(output).toEqual([ + '1: Script start', + '4: Script end', + '2: First timeout', + '3: Second timeout' + ]) + }) + }) + + // ============================================================ + // COMMON CALLBACK PATTERNS + // From callbacks.mdx lines 397-537 + // ============================================================ + + describe('Common Callback Patterns', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + // Pattern 2: Timers (lines 436-479) + + // From lines 440-447: setTimeout runs once after delay + it('should run setTimeout callback once after delay', async () => { + const output = [] + + // setTimeout - runs once after delay + const timeoutId = setTimeout(function() { + output.push('This runs once after 2 seconds') + }, 2000) + + expect(output).toEqual([]) + + await vi.advanceTimersByTimeAsync(2000) + + expect(output).toEqual(['This runs once after 2 seconds']) + }) + + // From lines 447: Cancel timeout before it runs + it('should cancel setTimeout with clearTimeout', async () => { + const callback = vi.fn() + + // Cancel it before it runs + const timeoutId = setTimeout(function() { + callback() + }, 2000) + + clearTimeout(timeoutId) + + await vi.advanceTimersByTimeAsync(2000) + + expect(callback).not.toHaveBeenCalled() + }) + + // From lines 449-459: setInterval runs repeatedly + it('should run setInterval callback repeatedly until cleared', async () => { + const output = [] + + // setInterval - runs repeatedly + let count = 0 + const intervalId = setInterval(function() { + count++ + output.push(`Count: ${count}`) + + if (count >= 5) { + clearInterval(intervalId) // Stop after 5 times + output.push('Done!') + } + }, 1000) + + await vi.advanceTimersByTimeAsync(5000) + + expect(output).toEqual([ + 'Count: 1', + 'Count: 2', + 'Count: 3', + 'Count: 4', + 'Count: 5', + 'Done!' + ]) + }) + + // From lines 464-479: Passing arguments to timer callbacks + it('should pass arguments to setTimeout callbacks using closure', async () => { + const output = [] + + // Method 1: Closure (most common) + const name = 'Alice' + setTimeout(function() { + output.push(`Hello, ${name}!`) + }, 1000) + + await vi.advanceTimersByTimeAsync(1000) + + expect(output).toEqual(['Hello, Alice!']) + }) + + it('should pass arguments to setTimeout callbacks using extra arguments', async () => { + const output = [] + + // Method 2: setTimeout's extra arguments + setTimeout(function(greeting, name) { + output.push(`${greeting}, ${name}!`) + }, 1000, 'Hello', 'Bob') // Extra args passed to callback + + await vi.advanceTimersByTimeAsync(1000) + + expect(output).toEqual(['Hello, Bob!']) + }) + + it('should pass arguments to setTimeout callbacks using arrow function with closure', async () => { + const output = [] + + // Method 3: Arrow function with closure + const user = { name: 'Charlie' } + setTimeout(() => output.push(`Hi, ${user.name}!`), 1000) + + await vi.advanceTimersByTimeAsync(1000) + + expect(output).toEqual(['Hi, Charlie!']) + }) + + // Pattern 3: Array Iteration (lines 481-512) + + // From lines 485-512: products array examples + it('should demonstrate array iteration callbacks with products array', () => { + const products = [ + { name: 'Laptop', price: 999, inStock: true }, + { name: 'Phone', price: 699, inStock: false }, + { name: 'Tablet', price: 499, inStock: true } + ] + + // forEach - do something with each item + const forEachOutput = [] + products.forEach(product => { + forEachOutput.push(`${product.name}: $${product.price}`) + }) + expect(forEachOutput).toEqual([ + 'Laptop: $999', + 'Phone: $699', + 'Tablet: $499' + ]) + + // map - transform each item into something new + const productNames = products.map(product => product.name) + // ['Laptop', 'Phone', 'Tablet'] + expect(productNames).toEqual(['Laptop', 'Phone', 'Tablet']) + + // filter - keep only items that pass a test + const available = products.filter(product => product.inStock) + // [{ name: 'Laptop', ... }, { name: 'Tablet', ... }] + expect(available).toEqual([ + { name: 'Laptop', price: 999, inStock: true }, + { name: 'Tablet', price: 499, inStock: true } + ]) + + // find - get the first item that passes a test + const phone = products.find(product => product.name === 'Phone') + // { name: 'Phone', price: 699, inStock: false } + expect(phone).toEqual({ name: 'Phone', price: 699, inStock: false }) + + // reduce - combine all items into a single value + const totalValue = products.reduce((sum, product) => sum + product.price, 0) + // 2197 + expect(totalValue).toBe(2197) + }) + + // Pattern 4: Custom Callbacks (lines 514-537) + + // From lines 518-537: fetchUserData custom callback + it('should demonstrate custom callback pattern with fetchUserData', async () => { + const output = [] + + // A function that does something and then calls you back + function fetchUserData(userId, callback) { + // Simulate async operation + setTimeout(function() { + const user = { id: userId, name: 'Alice', email: 'alice@example.com' } + callback(user) + }, 1000) + } + + // Using the function + fetchUserData(123, function(user) { + output.push(`Got user: ${user.name}`) + }) + output.push('Fetching user...') + + // Before timer fires + expect(output).toEqual(['Fetching user...']) + + await vi.advanceTimersByTimeAsync(1000) + + // Output: + // Fetching user... + // Got user: Alice (1 second later) + expect(output).toEqual(['Fetching user...', 'Got user: Alice']) + }) + }) + + // ============================================================ + // THE ERROR-FIRST CALLBACK PATTERN + // From callbacks.mdx lines 541-654 + // ============================================================ + + describe('Error-First Callback Pattern', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + // From lines 547-553: Error-first callback signature + it('should demonstrate error-first callback signature', () => { + // Error-first callback signature + // function callback(error, result) { + // // error: null/undefined if success, Error object if failure + // // result: the data if success, usually undefined if failure + // } + + let receivedError = 'not called' + let receivedResult = 'not called' + + function callback(error, result) { + receivedError = error + receivedResult = result + } + + // Success case + callback(null, 'success data') + expect(receivedError).toBeNull() + expect(receivedResult).toBe('success data') + + // Error case + callback(new Error('something failed'), undefined) + expect(receivedError).toBeInstanceOf(Error) + expect(receivedError.message).toBe('something failed') + expect(receivedResult).toBeUndefined() + }) + + // From lines 586-622: divideAsync error-first example + it('should demonstrate divideAsync error-first callback pattern', async () => { + function divideAsync(a, b, callback) { + // Simulate async operation + setTimeout(function() { + // Check for errors + if (typeof a !== 'number' || typeof b !== 'number') { + callback(new Error('Both arguments must be numbers')) + return + } + + if (b === 0) { + callback(new Error('Cannot divide by zero')) + return + } + + // Success! Error is null, result is the value + const result = a / b + callback(null, result) + }, 100) + } + + // Test success case + let successError = 'not called' + let successResult = 'not called' + + divideAsync(10, 2, function(error, result) { + successError = error + successResult = result + }) + + await vi.advanceTimersByTimeAsync(100) + + expect(successError).toBeNull() + expect(successResult).toBe(5) // Result: 5 + + // Test error case - divide by zero + let errorError = 'not called' + let errorResult = 'not called' + + divideAsync(10, 0, function(error, result) { + errorError = error + errorResult = result + }) + + await vi.advanceTimersByTimeAsync(100) + + expect(errorError).toBeInstanceOf(Error) + expect(errorError.message).toBe('Cannot divide by zero') + }) + + // From lines 627-650: Common Mistake - Forgetting to Return + it('should demonstrate the importance of returning after error callback', () => { + const results = [] + + // Wrong - doesn't return after error + function processDataWrong(data, callback) { + if (!data) { + callback(new Error('No data provided')) + // Oops! Execution continues... + } + + // This runs even when there's an error! + results.push('This should not run if error') + callback(null, 'processed') + } + + // Correct - return after error callback + function processDataCorrect(data, callback) { + if (!data) { + return callback(new Error('No data provided')) + // Or: callback(new Error(...)); return; + } + + // This only runs if data exists + results.push('This only runs on success') + callback(null, 'processed') + } + + // Test wrong way + processDataWrong(null, () => {}) + expect(results).toContain('This should not run if error') // Bug! + + results.length = 0 // Clear results + + // Test correct way + processDataCorrect(null, () => {}) + expect(results).not.toContain('This only runs on success') // Correct! + }) + }) + + // ============================================================ + // CALLBACK HELL: THE PYRAMID OF DOOM + // From callbacks.mdx lines 658-757 + // ============================================================ + + describe('Callback Hell', () => { + // From lines 674-715: Nested callback example + it('should demonstrate the pyramid of doom pattern', () => { + return new Promise((resolve) => { + const steps = [] + + // Simulated async operations + function getUser(userId, callback) { + setTimeout(() => { + steps.push('getUser') + callback(null, { id: userId, name: 'Alice' }) + }, 0) + } + + function verifyPassword(user, password, callback) { + setTimeout(() => { + steps.push('verifyPassword') + callback(null, password === 'correct') + }, 0) + } + + function getProfile(userId, callback) { + setTimeout(() => { + steps.push('getProfile') + callback(null, { bio: 'Developer' }) + }, 0) + } + + function getSettings(userId, callback) { + setTimeout(() => { + steps.push('getSettings') + callback(null, { theme: 'dark' }) + }, 0) + } + + function renderDashboard(user, profile, settings, callback) { + setTimeout(() => { + steps.push('renderDashboard') + callback(null) + }, 0) + } + + function handleError(error) { + steps.push(`Error: ${error.message}`) + } + + const userId = 123 + const password = 'correct' + + // Callback hell - nested callbacks (pyramid of doom) + getUser(userId, function(error, user) { + if (error) { + handleError(error) + return + } + + verifyPassword(user, password, function(error, isValid) { + if (error) { + handleError(error) + return + } + + if (!isValid) { + handleError(new Error('Invalid password')) + return + } + + getProfile(user.id, function(error, profile) { + if (error) { + handleError(error) + return + } + + getSettings(user.id, function(error, settings) { + if (error) { + handleError(error) + return + } + + renderDashboard(user, profile, settings, function(error) { + if (error) { + handleError(error) + return + } + + steps.push('Dashboard rendered!') + + expect(steps).toEqual([ + 'getUser', + 'verifyPassword', + 'getProfile', + 'getSettings', + 'renderDashboard', + 'Dashboard rendered!' + ]) + resolve() + }) + }) + }) + }) + }) + }) + }) + }) + + // ============================================================ + // ESCAPING CALLBACK HELL + // From callbacks.mdx lines 761-970 + // ============================================================ + + describe('Escaping Callback Hell', () => { + // From lines 769-801: Strategy 1 - Named Functions + it('should demonstrate named functions to escape callback hell', () => { + return new Promise((resolve) => { + const steps = [] + let rejected = false + + function getData(callback) { + setTimeout(() => { + steps.push('getData') + callback(null, 'data') + }, 0) + } + + function processData(data, callback) { + setTimeout(() => { + steps.push(`processData: ${data}`) + callback(null, 'processed') + }, 0) + } + + function saveData(processed, callback) { + setTimeout(() => { + steps.push(`saveData: ${processed}`) + callback(null) + }, 0) + } + + function handleError(err) { + steps.push(`Error: ${err.message}`) + rejected = true + } + + // After: Named functions + function handleData(err, data) { + if (err) return handleError(err) + processData(data, handleProcessed) + } + + function handleProcessed(err, processed) { + if (err) return handleError(err) + saveData(processed, handleSaved) + } + + function handleSaved(err) { + if (err) return handleError(err) + steps.push('Done!') + + expect(steps).toEqual([ + 'getData', + 'processData: data', + 'saveData: processed', + 'Done!' + ]) + resolve() + } + + // Start the chain + getData(handleData) + }) + }) + + // From lines 813-847: Strategy 2 - Early Returns + it('should demonstrate early returns to reduce nesting', () => { + return new Promise((resolve) => { + const results = [] + + function validateUser(user, callback) { + setTimeout(() => { + callback(null, user.name !== '') + }, 0) + } + + function saveUser(user, callback) { + setTimeout(() => { + callback(null, { ...user, saved: true }) + }, 0) + } + + // Use early returns + function processUser(user, callback) { + validateUser(user, function(err, isValid) { + if (err) return callback(err) + if (!isValid) return callback(new Error('Invalid user')) + + saveUser(user, function(err, savedUser) { + if (err) return callback(err) + callback(null, savedUser) + }) + }) + } + + processUser({ name: 'Alice' }, function(err, result) { + expect(err).toBeNull() + expect(result).toEqual({ name: 'Alice', saved: true }) + + // Test invalid user + processUser({ name: '' }, function(err, result) { + expect(err).toBeInstanceOf(Error) + expect(err.message).toBe('Invalid user') + resolve() + }) + }) + }) + }) + + // From lines 853-888: Strategy 3 - Modularization + it('should demonstrate modularization to break up callback hell', () => { + return new Promise((resolve) => { + const steps = [] + + // auth.js + function getUser(email, callback) { + setTimeout(() => callback(null, { id: 1, email }), 0) + } + + function verifyPassword(user, password, callback) { + setTimeout(() => callback(null, password === 'secret'), 0) + } + + function authenticateUser(credentials, callback) { + getUser(credentials.email, function(err, user) { + if (err) return callback(err) + + verifyPassword(user, credentials.password, function(err, isValid) { + if (err) return callback(err) + if (!isValid) return callback(new Error('Invalid password')) + callback(null, user) + }) + }) + } + + // profile.js + function getProfile(userId, callback) { + setTimeout(() => callback(null, { bio: 'Developer' }), 0) + } + + function getSettings(userId, callback) { + setTimeout(() => callback(null, { theme: 'dark' }), 0) + } + + function loadUserProfile(userId, callback) { + getProfile(userId, function(err, profile) { + if (err) return callback(err) + + getSettings(userId, function(err, settings) { + if (err) return callback(err) + callback(null, { profile, settings }) + }) + }) + } + + function handleError(err) { + steps.push(`Error: ${err.message}`) + } + + function renderDashboard(user, profile, settings) { + steps.push(`Rendered dashboard for ${user.email}`) + } + + // main.js + const credentials = { email: 'alice@example.com', password: 'secret' } + + authenticateUser(credentials, function(err, user) { + if (err) return handleError(err) + + loadUserProfile(user.id, function(err, data) { + if (err) return handleError(err) + renderDashboard(user, data.profile, data.settings) + + expect(steps).toEqual(['Rendered dashboard for alice@example.com']) + resolve() + }) + }) + }) + }) + }) + + // ============================================================ + // COMMON CALLBACK MISTAKES + // From callbacks.mdx lines 974-1121 + // ============================================================ + + describe('Common Callback Mistakes', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + // From lines 981-1001: Mistake 1 - Calling a Callback Multiple Times + it('should demonstrate the problem of calling callbacks multiple times', () => { + const results = [] + + // Wrong - callback called multiple times! + function fetchDataWrong(url, callback) { + // Simulating the wrong pattern from the docs + callback(null, 'response') // Called on success + // In the wrong code, .finally() would also call callback + callback(null, 'done') // Called ALWAYS - even after success or error! + } + + fetchDataWrong('http://example.com', (err, data) => { + results.push(data) + }) + + // Bug: callback was called twice! + expect(results).toEqual(['response', 'done']) + expect(results.length).toBe(2) + }) + + // From lines 1003-1041: Mistake 2 - Zalgo (sync/async inconsistency) + it('should demonstrate the Zalgo problem (inconsistent sync/async)', async () => { + const cache = new Map() + const order = [] + + // Wrong - sometimes sync, sometimes async (Zalgo!) + function getData(key, callback) { + if (cache.has(key)) { + callback(null, cache.get(key)) // Sync! + return + } + + setTimeout(() => { + const data = `data for ${key}` + cache.set(key, data) + callback(null, data) // Async! + }, 0) + } + + // This causes unpredictable behavior: + let value = 'initial' + getData('key1', function(err, data) { + value = data + }) + order.push(`After first call: ${value}`) + + await vi.advanceTimersByTimeAsync(0) + order.push(`After timer: ${value}`) + + // Second call - from cache (sync) + getData('key1', function(err, data) { + value = 'from cache' + }) + order.push(`After second call: ${value}`) + + // Inconsistent! First call: value changed after timer + // Second call: value changed immediately + expect(order).toEqual([ + 'After first call: initial', // First call was async + 'After timer: data for key1', // Value updated after timer + 'After second call: from cache' // Second call was sync - immediate! + ]) + }) + + // From lines 1027-1041: Solution to Zalgo - always be async + it('should demonstrate the solution to Zalgo - always async', async () => { + const cache = new Map() + const order = [] + + // Correct - always async + function getData(key, callback) { + if (cache.has(key)) { + // Use setTimeout to make it async even when cached + setTimeout(function() { + callback(null, cache.get(key)) + }, 0) + return + } + + setTimeout(() => { + const data = `data for ${key}` + cache.set(key, data) + callback(null, data) + }, 0) + } + + let value = 'initial' + + // First call + getData('key1', function(err, data) { + value = data + order.push(`callback1: ${value}`) + }) + order.push('after first call') + + await vi.advanceTimersByTimeAsync(0) + + // Second call (from cache, but still async) + getData('key1', function(err, data) { + value = 'from cache' + order.push(`callback2: ${value}`) + }) + order.push('after second call') + + await vi.advanceTimersByTimeAsync(0) + + // Consistent ordering! Both callbacks run after their respective calls + expect(order).toEqual([ + 'after first call', + 'callback1: data for key1', + 'after second call', + 'callback2: from cache' + ]) + }) + + // From lines 1043-1092: Mistake 3 - Losing `this` Context + it('should demonstrate losing this context with regular function callbacks', async () => { + // Wrong - this is undefined/global + const user = { + name: 'Alice', + greetLater: function() { + return new Promise(resolve => { + setTimeout(function() { + // 'this' is undefined in strict mode + resolve(this?.name) // this.name is undefined! + }, 1000) + }) + } + } + + const promise = user.greetLater() + await vi.advanceTimersByTimeAsync(1000) + const result = await promise + + expect(result).toBeUndefined() // "Hello, undefined!" + }) + + it('should preserve this context with arrow function callbacks', async () => { + // Correct - Use arrow function (inherits this) + const user = { + name: 'Alice', + greetLater: function() { + return new Promise(resolve => { + setTimeout(() => { + resolve(`Hello, ${this.name}!`) // Arrow function keeps this + }, 1000) + }) + } + } + + const promise = user.greetLater() + await vi.advanceTimersByTimeAsync(1000) + const result = await promise + + expect(result).toBe('Hello, Alice!') + }) + + it('should preserve this context with bind', async () => { + // Correct - Use bind + const user = { + name: 'Alice', + greetLater: function() { + return new Promise(resolve => { + setTimeout(function() { + resolve(`Hello, ${this.name}!`) + }.bind(this), 1000) // Explicitly bind this + }) + } + } + + const promise = user.greetLater() + await vi.advanceTimersByTimeAsync(1000) + const result = await promise + + expect(result).toBe('Hello, Alice!') + }) + + it('should preserve this context by saving reference', async () => { + // Correct - Save reference to this + const user = { + name: 'Alice', + greetLater: function() { + const self = this // Save reference + return new Promise(resolve => { + setTimeout(function() { + resolve(`Hello, ${self.name}!`) + }, 1000) + }) + } + } + + const promise = user.greetLater() + await vi.advanceTimersByTimeAsync(1000) + const result = await promise + + expect(result).toBe('Hello, Alice!') + }) + }) + + // ============================================================ + // TEST YOUR KNOWLEDGE + // From callbacks.mdx lines 1260-1371 + // ============================================================ + + describe('Test Your Knowledge', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + // From lines 1260-1285: Question 3 - What's the output of this code? + it('Question 3: What is the output order? A, C, E, B, D', async () => { + const output = [] + + output.push('A') + + setTimeout(() => output.push('B'), 0) + + output.push('C') + + setTimeout(() => output.push('D'), 0) + + output.push('E') + + // Before timers: A, C, E + expect(output).toEqual(['A', 'C', 'E']) + + await vi.advanceTimersByTimeAsync(0) + + // Answer: A, C, E, B, D + // + // Explanation: + // 1. console.log('A') - sync, runs immediately -> "A" + // 2. setTimeout(..., 0) - registers callback B, continues + // 3. console.log('C') - sync, runs immediately -> "C" + // 4. setTimeout(..., 0) - registers callback D, continues + // 5. console.log('E') - sync, runs immediately -> "E" + // 6. Call stack empty -> event loop runs callback B -> "B" + // 7. Event loop runs callback D -> "D" + // + // Even with 0ms delay, setTimeout callbacks run after all sync code. + expect(output).toEqual(['A', 'C', 'E', 'B', 'D']) + }) + + // From lines 1287-1316: Question 4 - How can you preserve `this` context? + it('Question 4: Three ways to preserve this context', async () => { + // 1. Arrow functions (recommended) + const obj1 = { + name: 'Alice', + greet() { + return new Promise(resolve => { + setTimeout(() => { + resolve(this.name) // "Alice" + }, 100) + }) + } + } + + // 2. Using bind() + const obj2 = { + name: 'Alice', + greet() { + return new Promise(resolve => { + setTimeout(function() { + resolve(this.name) + }.bind(this), 100) + }) + } + } + + // 3. Saving a reference + const obj3 = { + name: 'Alice', + greet() { + const self = this + return new Promise(resolve => { + setTimeout(function() { + resolve(self.name) + }, 100) + }) + } + } + + const promise1 = obj1.greet() + const promise2 = obj2.greet() + const promise3 = obj3.greet() + + await vi.advanceTimersByTimeAsync(100) + + expect(await promise1).toBe('Alice') + expect(await promise2).toBe('Alice') + expect(await promise3).toBe('Alice') + }) + + // From lines 1318-1342: Question 5 - Why can't you use try/catch with async callbacks? + it('Question 5: try/catch cannot catch async callback errors', async () => { + // The try/catch block executes synchronously. By the time an async + // callback runs, the try/catch is long gone - it's on a different + // "turn" of the event loop. + + let tryCatchExecuted = false + const callbackExecuted = vi.fn() + + try { + setTimeout(() => { + callbackExecuted() + // throw new Error('Async error!') // This escapes! + }, 100) + } catch (e) { + // This NEVER catches the error + tryCatchExecuted = true + } + + // The error crashes the program because: + // 1. try/catch runs immediately + // 2. setTimeout registers callback and returns + // 3. try/catch completes (nothing thrown yet!) + // 4. 100ms later, callback runs and throws + // 5. No try/catch exists at that point + + expect(tryCatchExecuted).toBe(false) + expect(callbackExecuted).not.toHaveBeenCalled() + + await vi.advanceTimersByTimeAsync(100) + + // Callback ran, but try/catch is long gone + expect(callbackExecuted).toHaveBeenCalled() + expect(tryCatchExecuted).toBe(false) // Still false! + }) + + // From lines 1344-1370: Question 6 - Three ways to avoid callback hell + it('Question 6: Three ways to avoid callback hell', async () => { + const steps = [] + + function getUser(userId, callback) { + setTimeout(() => callback(null, { id: userId, name: 'Alice' }), 0) + } + + function getProfile(userId, callback) { + setTimeout(() => callback(null, { bio: 'Developer' }), 0) + } + + function handleError(err) { + steps.push(`Error: ${err.message}`) + } + + // 1. Named functions - Extract callbacks into named functions + function handleUser(err, user) { + if (err) return handleError(err) + getProfile(user.id, handleProfile) + } + + function handleProfile(err, profile) { + if (err) return handleError(err) + steps.push(`Got profile: ${profile.bio}`) + } + + // Start the chain + getUser(123, handleUser) + + // Advance timers to let callbacks execute + // Need to run all pending timers (nested setTimeouts) + await vi.runAllTimersAsync() + + expect(steps).toEqual(['Got profile: Developer']) + + // Other approaches mentioned in docs: + // 2. Modularization - Split into separate modules/functions + // 3. Promises/async-await - Use modern async patterns + }) + }) + + // ============================================================ + // ADDITIONAL EDGE CASES + // ============================================================ + + describe('Additional Edge Cases', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + it('should handle nested setTimeout callbacks', async () => { + const order = [] + + setTimeout(() => { + order.push('first') + setTimeout(() => { + order.push('second') + setTimeout(() => { + order.push('third') + }, 100) + }, 100) + }, 100) + + await vi.advanceTimersByTimeAsync(100) + expect(order).toEqual(['first']) + + await vi.advanceTimersByTimeAsync(100) + expect(order).toEqual(['first', 'second']) + + await vi.advanceTimersByTimeAsync(100) + expect(order).toEqual(['first', 'second', 'third']) + }) + + it('should demonstrate callback with multiple array methods chained', () => { + const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + + // Each method accepts a callback + const result = numbers + .filter(n => n % 2 === 0) // Keep evens: [2, 4, 6, 8, 10] + .map(n => n * 2) // Double: [4, 8, 12, 16, 20] + .reduce((sum, n) => sum + n, 0) // Sum: 60 + + expect(result).toBe(60) + }) + + it('should demonstrate once-only callback pattern', () => { + function once(callback) { + let called = false + return function(...args) { + if (called) return + called = true + return callback.apply(this, args) + } + } + + let callCount = 0 + const onceCallback = once(() => { + callCount++ + return 'result' + }) + + expect(onceCallback()).toBe('result') + expect(onceCallback()).toBeUndefined() + expect(onceCallback()).toBeUndefined() + expect(callCount).toBe(1) + }) + + it('should demonstrate closure issues with var in loops', async () => { + const results = [] + + // Wrong with var - all callbacks see final value + for (var i = 0; i < 3; i++) { + setTimeout(() => results.push(`var: ${i}`), 100) + } + + await vi.advanceTimersByTimeAsync(100) + + // All see i = 3 (the final value after loop completes) + expect(results).toEqual(['var: 3', 'var: 3', 'var: 3']) + }) + + it('should demonstrate closure fix with let in loops', async () => { + const results = [] + + // Correct with let - each iteration gets its own i + for (let i = 0; i < 3; i++) { + setTimeout(() => results.push(`let: ${i}`), 100) + } + + await vi.advanceTimersByTimeAsync(100) + + expect(results).toEqual(['let: 0', 'let: 1', 'let: 2']) + }) + }) +}) diff --git a/tests/functional-programming/currying-composition/currying-composition.test.js b/tests/functional-programming/currying-composition/currying-composition.test.js new file mode 100644 index 00000000..fc7d9425 --- /dev/null +++ b/tests/functional-programming/currying-composition/currying-composition.test.js @@ -0,0 +1,981 @@ +import { describe, it, expect } from 'vitest' + +describe('Currying & Composition', () => { + describe('Basic Currying', () => { + describe('Manual Currying with Arrow Functions', () => { + it('should create a curried function with arrow syntax', () => { + const add = a => b => c => a + b + c + + expect(add(1)(2)(3)).toBe(6) + }) + + it('should allow partial application at each step', () => { + const add = a => b => c => a + b + c + + const add1 = add(1) // Returns b => c => 1 + b + c + const add1and2 = add1(2) // Returns c => 1 + 2 + c + const result = add1and2(3) // Returns 6 + + expect(typeof add1).toBe('function') + expect(typeof add1and2).toBe('function') + expect(result).toBe(6) + }) + + it('should demonstrate closures preserving arguments', () => { + const multiply = a => b => a * b + + const double = multiply(2) + const triple = multiply(3) + + expect(double(5)).toBe(10) + expect(triple(5)).toBe(15) + expect(double(10)).toBe(20) + expect(triple(10)).toBe(30) + }) + }) + + describe('Traditional Function Currying', () => { + it('should work with traditional function syntax', () => { + function curriedAdd(a) { + return function(b) { + return function(c) { + return a + b + c + } + } + } + + expect(curriedAdd(1)(2)(3)).toBe(6) + }) + }) + + describe('Pizza Restaurant Example', () => { + it('should demonstrate the pizza ordering pattern', () => { + const orderPizza = size => crust => topping => { + return `${size} ${crust}-crust ${topping} pizza` + } + + expect(orderPizza("Large")("Thin")("Pepperoni")) + .toBe("Large Thin-crust Pepperoni pizza") + }) + + it('should allow creating reusable order templates', () => { + const orderPizza = size => crust => topping => { + return `${size} ${crust}-crust ${topping} pizza` + } + + const orderLarge = orderPizza("Large") + const orderLargeThin = orderLarge("Thin") + + expect(orderLargeThin("Mushroom")).toBe("Large Thin-crust Mushroom pizza") + expect(orderLargeThin("Hawaiian")).toBe("Large Thin-crust Hawaiian pizza") + }) + }) + }) + + describe('Curry Helper Implementation', () => { + describe('Basic Two-Argument Curry', () => { + it('should curry a two-argument function', () => { + function curry(fn) { + return function(a) { + return function(b) { + return fn(a, b) + } + } + } + + const add = (a, b) => a + b + const curriedAdd = curry(add) + + expect(curriedAdd(1)(2)).toBe(3) + }) + }) + + describe('Advanced Curry (Any Number of Arguments)', () => { + const curry = fn => { + return function curried(...args) { + if (args.length >= fn.length) { + return fn.apply(this, args) + } + return (...nextArgs) => curried.apply(this, args.concat(nextArgs)) + } + } + + it('should support full currying', () => { + const sum = (a, b, c) => a + b + c + const curriedSum = curry(sum) + + expect(curriedSum(1)(2)(3)).toBe(6) + }) + + it('should support normal function calls', () => { + const sum = (a, b, c) => a + b + c + const curriedSum = curry(sum) + + expect(curriedSum(1, 2, 3)).toBe(6) + }) + + it('should support mixed calling styles', () => { + const sum = (a, b, c) => a + b + c + const curriedSum = curry(sum) + + expect(curriedSum(1, 2)(3)).toBe(6) + expect(curriedSum(1)(2, 3)).toBe(6) + }) + + it('should work with functions of different arities', () => { + const add2 = (a, b) => a + b + const add4 = (a, b, c, d) => a + b + c + d + + const curriedAdd2 = curry(add2) + const curriedAdd4 = curry(add4) + + expect(curriedAdd2(1)(2)).toBe(3) + expect(curriedAdd4(1)(2)(3)(4)).toBe(10) + expect(curriedAdd4(1, 2)(3, 4)).toBe(10) + }) + }) + + describe('curryN (Explicit Arity)', () => { + const curryN = (fn, arity) => { + return function curried(...args) { + if (args.length >= arity) { + return fn(...args) + } + return (...nextArgs) => curried(...args, ...nextArgs) + } + } + + it('should curry variadic functions with explicit arity', () => { + const sum = (...nums) => nums.reduce((a, b) => a + b, 0) + + const curriedSum3 = curryN(sum, 3) + const curriedSum5 = curryN(sum, 5) + + expect(curriedSum3(1)(2)(3)).toBe(6) + expect(curriedSum5(1)(2)(3)(4)(5)).toBe(15) + }) + }) + }) + + describe('Currying vs Partial Application', () => { + describe('Currying (One Argument at a Time)', () => { + it('should demonstrate currying with unary functions', () => { + const curriedAdd = a => b => c => a + b + c + + // Each call takes exactly ONE argument + const step1 = curriedAdd(1) // Returns function + const step2 = step1(2) // Returns function + const step3 = step2(3) // Returns 6 + + expect(typeof step1).toBe('function') + expect(typeof step2).toBe('function') + expect(step3).toBe(6) + }) + }) + + describe('Partial Application (Fix Some Args)', () => { + const partial = (fn, ...presetArgs) => { + return (...laterArgs) => fn(...presetArgs, ...laterArgs) + } + + it('should fix some arguments upfront', () => { + const greet = (greeting, punctuation, name) => { + return `${greeting}, ${name}${punctuation}` + } + + const greetExcitedly = partial(greet, "Hello", "!") + + expect(greetExcitedly("Alice")).toBe("Hello, Alice!") + expect(greetExcitedly("Bob")).toBe("Hello, Bob!") + }) + + it('should take remaining arguments together, not one at a time', () => { + const add = (a, b, c, d) => a + b + c + d + + const add10 = partial(add, 10) + + // Takes remaining 3 args at once + expect(add10(1, 2, 3)).toBe(16) + }) + + it('should differ from currying in how arguments are collected', () => { + const add = (a, b, c) => a + b + c + + // Curried: takes args one at a time + const curriedAdd = a => b => c => a + b + c + + // Partial: fixes some args, takes rest together + const add1 = partial(add, 1) + + // Curried needs 3 calls + expect(curriedAdd(1)(2)(3)).toBe(6) + + // Partial takes remaining in one call + expect(add1(2, 3)).toBe(6) + }) + }) + }) + + describe('Real-World Currying Patterns', () => { + describe('Configurable Logger', () => { + it('should create specialized loggers', () => { + const logs = [] + + const createLogger = level => prefix => message => { + const logEntry = `[${level}] ${prefix}: ${message}` + logs.push(logEntry) + return logEntry + } + + const infoLogger = createLogger('INFO')('App') + const errorLogger = createLogger('ERROR')('App') + + expect(infoLogger('Started')).toBe('[INFO] App: Started') + expect(errorLogger('Failed')).toBe('[ERROR] App: Failed') + }) + }) + + describe('API Client Factory', () => { + it('should create specialized API clients', () => { + const createApiUrl = baseUrl => endpoint => params => { + const queryString = new URLSearchParams(params).toString() + return `${baseUrl}${endpoint}${queryString ? '?' + queryString : ''}` + } + + const githubApi = createApiUrl('https://api.github.com') + const getUsers = githubApi('/users') + + expect(getUsers({})).toBe('https://api.github.com/users') + expect(getUsers({ per_page: 10 })).toBe('https://api.github.com/users?per_page=10') + }) + }) + + describe('Validation Functions', () => { + it('should create reusable validators', () => { + const isGreaterThan = min => value => value > min + const isLessThan = max => value => value < max + const hasLength = length => str => str.length === length + + const isAdult = isGreaterThan(17) + const isValidAge = isLessThan(120) + const isValidZipCode = hasLength(5) + + expect(isAdult(18)).toBe(true) + expect(isAdult(15)).toBe(false) + expect(isValidAge(50)).toBe(true) + expect(isValidAge(150)).toBe(false) + expect(isValidZipCode('12345')).toBe(true) + expect(isValidZipCode('1234')).toBe(false) + }) + + it('should work with array methods', () => { + const isGreaterThan = min => value => value > min + const isAdult = isGreaterThan(17) + + const ages = [15, 22, 45, 8, 67] + const adults = ages.filter(isAdult) + + expect(adults).toEqual([22, 45, 67]) + }) + }) + + describe('Discount Calculator', () => { + it('should create specialized discount functions', () => { + const applyDiscount = discountPercent => price => { + return price * (1 - discountPercent / 100) + } + + const tenPercentOff = applyDiscount(10) + const twentyPercentOff = applyDiscount(20) + const blackFridayDeal = applyDiscount(50) + + expect(tenPercentOff(100)).toBe(90) + expect(twentyPercentOff(100)).toBe(80) + expect(blackFridayDeal(100)).toBe(50) + }) + + it('should work with array map', () => { + const applyDiscount = discountPercent => price => { + return price * (1 - discountPercent / 100) + } + + const tenPercentOff = applyDiscount(10) + const prices = [100, 200, 50, 75] + + const discountedPrices = prices.map(tenPercentOff) + + expect(discountedPrices).toEqual([90, 180, 45, 67.5]) + }) + }) + + describe('Event Handler Configuration', () => { + it('should configure event handlers step by step', () => { + const handlers = [] + + const handleEvent = eventType => elementId => callback => { + const handler = { eventType, elementId, callback } + handlers.push(handler) + return handler + } + + const onClick = handleEvent('click') + const onClickButton = onClick('myButton') + + const handler = onClickButton(() => 'clicked!') + + expect(handler.eventType).toBe('click') + expect(handler.elementId).toBe('myButton') + expect(handler.callback()).toBe('clicked!') + }) + }) + }) + + describe('Function Composition', () => { + describe('pipe() Implementation', () => { + const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x) + + it('should compose functions left-to-right', () => { + const add1 = x => x + 1 + const double = x => x * 2 + const square = x => x * x + + const process = pipe(add1, double, square) + + // 5 → 6 → 12 → 144 + expect(process(5)).toBe(144) + }) + + it('should process single functions', () => { + const double = x => x * 2 + const process = pipe(double) + + expect(process(5)).toBe(10) + }) + + it('should handle identity when empty', () => { + const pipe = (...fns) => x => fns.length ? fns.reduce((acc, fn) => fn(acc), x) : x + const identity = pipe() + + expect(identity(5)).toBe(5) + }) + }) + + describe('compose() Implementation', () => { + const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x) + + it('should compose functions right-to-left', () => { + const add1 = x => x + 1 + const double = x => x * 2 + const square = x => x * x + + // Functions listed in reverse execution order + const process = compose(square, double, add1) + + // 5 → 6 → 12 → 144 (same result as pipe(add1, double, square)) + expect(process(5)).toBe(144) + }) + + it('should be equivalent to pipe with reversed arguments', () => { + const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x) + const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x) + + const add1 = x => x + 1 + const double = x => x * 2 + + const piped = pipe(add1, double) + const composed = compose(double, add1) + + expect(piped(5)).toBe(composed(5)) + }) + }) + + describe('String Transformation Pipeline', () => { + const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x) + + it('should transform strings through a pipeline', () => { + const getName = obj => obj.name + const trim = str => str.trim() + const toUpperCase = str => str.toUpperCase() + const addExclaim = str => str + '!' + + const shout = pipe(getName, trim, toUpperCase, addExclaim) + + expect(shout({ name: ' alice ' })).toBe('ALICE!') + }) + + it('should convert to camelCase', () => { + const trim = str => str.trim() + const toLowerCase = str => str.toLowerCase() + const splitWords = str => str.split(' ') + const capitalizeFirst = words => words.map((w, i) => + i === 0 ? w : w[0].toUpperCase() + w.slice(1) + ) + const joinWords = words => words.join('') + + const toCamelCase = pipe( + trim, + toLowerCase, + splitWords, + capitalizeFirst, + joinWords + ) + + expect(toCamelCase(' HELLO WORLD ')).toBe('helloWorld') + expect(toCamelCase('my variable name')).toBe('myVariableName') + }) + }) + + describe('Data Transformation Pipeline', () => { + const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x) + + it('should process array data through pipeline', () => { + const users = [ + { name: 'Alice', age: 25, active: true }, + { name: 'Bob', age: 17, active: true }, + { name: 'Charlie', age: 30, active: false }, + { name: 'Diana', age: 22, active: true } + ] + + const processUsers = pipe( + users => users.filter(u => u.active), + users => users.filter(u => u.age >= 18), + users => users.map(u => u.name), + names => names.sort() + ) + + expect(processUsers(users)).toEqual(['Alice', 'Diana']) + }) + }) + }) + + describe('Currying + Composition Together', () => { + const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x) + + describe('Data-Last Parameter Order', () => { + it('should enable composition with data-last curried functions', () => { + const map = fn => arr => arr.map(fn) + const filter = fn => arr => arr.filter(fn) + + const double = x => x * 2 + const isEven = x => x % 2 === 0 + + const doubleEvens = pipe( + filter(isEven), + map(double) + ) + + expect(doubleEvens([1, 2, 3, 4, 5, 6])).toEqual([4, 8, 12]) + }) + + it('should show why data-first is harder to compose', () => { + // Data-first: harder to compose + const mapFirst = (arr, fn) => arr.map(fn) + const filterFirst = (arr, fn) => arr.filter(fn) + + // Can't easily pipe these without wrapping + const double = x => x * 2 + const isEven = x => x % 2 === 0 + + // Would need manual wrapping: + const result = mapFirst(filterFirst([1, 2, 3, 4, 5, 6], isEven), double) + expect(result).toEqual([4, 8, 12]) + }) + }) + + describe('Curried Functions in Pipelines', () => { + it('should compose curried arithmetic functions', () => { + const add = a => b => a + b + const multiply = a => b => a * b + const subtract = a => b => b - a + + const add5 = add(5) + const double = multiply(2) + const subtract3 = subtract(3) + + const process = pipe(add5, double, subtract3) + + // 10 → 15 → 30 → 27 + expect(process(10)).toBe(27) + }) + + it('should demonstrate point-free style', () => { + const prop = key => obj => obj[key] + const toUpper = str => str.toUpperCase() + + // Point-free: no explicit data parameter + const getUpperName = pipe( + prop('name'), + toUpper + ) + + expect(getUpperName({ name: 'alice' })).toBe('ALICE') + expect(getUpperName({ name: 'bob' })).toBe('BOB') + }) + }) + + describe('Complex Pipeline with Currying', () => { + it('should process user data through curried pipeline', () => { + const prop = key => obj => obj[key] + const map = fn => arr => arr.map(fn) + const filter = pred => arr => arr.filter(pred) + const sort = compareFn => arr => [...arr].sort(compareFn) + const take = n => arr => arr.slice(0, n) + + const users = [ + { id: 1, name: 'Zara', score: 85 }, + { id: 2, name: 'Alice', score: 92 }, + { id: 3, name: 'Bob', score: 78 }, + { id: 4, name: 'Charlie', score: 95 } + ] + + const getTopScorers = pipe( + filter(u => u.score >= 80), + sort((a, b) => b.score - a.score), + take(2), + map(prop('name')) + ) + + expect(getTopScorers(users)).toEqual(['Charlie', 'Alice']) + }) + }) + }) + + describe('Interview Questions', () => { + describe('Implement sum(1)(2)(3)...(n)()', () => { + it('should return sum when called with no arguments', () => { + function sum(a) { + return function next(b) { + if (b === undefined) { + return a + } + return sum(a + b) + } + } + + expect(sum(1)(2)(3)()).toBe(6) + expect(sum(1)(2)(3)(4)(5)()).toBe(15) + expect(sum(10)()).toBe(10) + }) + }) + + describe('Infinite Currying with valueOf', () => { + it('should return sum when coerced to number', () => { + function sum(a) { + const fn = b => sum(a + b) + fn.valueOf = () => a + return fn + } + + expect(+sum(1)(2)(3)).toBe(6) + expect(+sum(1)(2)(3)(4)(5)).toBe(15) + }) + }) + + describe('Fix map + parseInt Issue', () => { + it('should demonstrate the problem', () => { + const result = ['1', '2', '3'].map(parseInt) + // parseInt receives (value, index, array) + // parseInt('1', 0) → 1 + // parseInt('2', 1) → NaN (base 1 is invalid) + // parseInt('3', 2) → NaN (3 is not valid in base 2) + expect(result).toEqual([1, NaN, NaN]) + }) + + it('should fix with unary wrapper', () => { + const unary = fn => arg => fn(arg) + + const result = ['1', '2', '3'].map(unary(parseInt)) + expect(result).toEqual([1, 2, 3]) + }) + }) + }) + + describe('Common Mistakes', () => { + describe('Forgetting Curried Functions Return Functions', () => { + it('should demonstrate the mistake', () => { + const add = a => b => a + b + + // Mistake: forgot second call + const result = add(1) + + expect(typeof result).toBe('function') + expect(result).not.toBe(1) // Not a number! + + // Correct + expect(add(1)(2)).toBe(3) + }) + }) + + describe('fn.length with Rest Parameters', () => { + it('should show fn.length is 0 for rest parameters', () => { + function withRest(...args) { + return args.reduce((a, b) => a + b, 0) + } + + function withDefault(a, b = 2) { + return a + b + } + + expect(withRest.length).toBe(0) + expect(withDefault.length).toBe(1) // Only counts params before default + }) + }) + + describe('Type Mismatches in Pipelines', () => { + const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x) + + it('should show type mismatch issues', () => { + const getAge = obj => obj.age // Returns number + const getLength = arr => arr.length // Expects array + + // This would cause issues + const broken = pipe(getAge, getLength) + + // Numbers have no .length property + expect(broken({ age: 25 })).toBe(undefined) + }) + }) + }) + + describe('Vanilla JS Utility Functions', () => { + describe('Complete Utility Set', () => { + // Curry + const curry = fn => { + return function curried(...args) { + return args.length >= fn.length + ? fn(...args) + : (...next) => curried(...args, ...next) + } + } + + // Pipe and Compose + const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x) + const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x) + + // Partial Application + const partial = (fn, ...presetArgs) => (...laterArgs) => fn(...presetArgs, ...laterArgs) + + // Data-last utilities + const map = fn => arr => arr.map(fn) + const filter = fn => arr => arr.filter(fn) + const reduce = (fn, initial) => arr => arr.reduce(fn, initial) + + it('should demonstrate all utilities working together', () => { + const sum = (a, b, c) => a + b + c + const curriedSum = curry(sum) + + expect(curriedSum(1)(2)(3)).toBe(6) + expect(curriedSum(1, 2)(3)).toBe(6) + + const double = x => x * 2 + const add1 = x => x + 1 + + const process = pipe(add1, double) + expect(process(5)).toBe(12) + + const processReverse = compose(add1, double) + expect(processReverse(5)).toBe(11) // double first, then add1 + + const greet = (greeting, name) => `${greeting}, ${name}!` + const sayHello = partial(greet, 'Hello') + expect(sayHello('Alice')).toBe('Hello, Alice!') + + const nums = [1, 2, 3, 4, 5] + const isEven = x => x % 2 === 0 + + const sumOfDoubledEvens = pipe( + filter(isEven), + map(double), + reduce((a, b) => a + b, 0) + ) + + expect(sumOfDoubledEvens(nums)).toBe(12) // [2,4] → [4,8] → 12 + }) + }) + }) + + describe('Practical Examples from Documentation', () => { + const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x) + + describe('Logging Factory', () => { + it('should create specialized loggers from documentation example', () => { + const logs = [] + + const createLogger = level => withTimestamp => message => { + const timestamp = withTimestamp ? '2024-01-15T10:30:00Z' : '' + const logEntry = `[${level}]${timestamp ? ' ' + timestamp : ''} ${message}` + logs.push(logEntry) + return logEntry + } + + const info = createLogger('INFO')(true) + const quickLog = createLogger('LOG')(false) + + expect(info('Application started')).toBe('[INFO] 2024-01-15T10:30:00Z Application started') + expect(quickLog('Quick debug')).toBe('[LOG] Quick debug') + }) + }) + + describe('Assembly Line Pipeline', () => { + it('should transform user data as shown in documentation', () => { + const getName = obj => obj.name + const trim = str => str.trim() + const toLowerCase = str => str.toLowerCase() + + const processUser = pipe(getName, trim, toLowerCase) + + expect(processUser({ name: ' ALICE ' })).toBe('alice') + }) + }) + }) + + describe('Additional Documentation Examples', () => { + const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x) + const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x) + + describe('Real-World Pipeline: Processing API Data (doc lines 729-756)', () => { + it('should process API response through complete pipeline', () => { + // Mock API response matching documentation example + const apiResponse = { + data: [ + { id: 1, firstName: 'Charlie', lastName: 'Brown', email: 'charlie@test.com', isActive: true }, + { id: 2, firstName: 'Alice', lastName: 'Smith', email: 'alice@test.com', isActive: false }, + { id: 3, firstName: 'Bob', lastName: 'Jones', email: 'bob@test.com', isActive: true }, + { id: 4, firstName: 'Diana', lastName: 'Prince', email: 'diana@test.com', isActive: true }, + { id: 5, firstName: 'Eve', lastName: 'Wilson', email: 'eve@test.com', isActive: true }, + { id: 6, firstName: 'Frank', lastName: 'Miller', email: 'frank@test.com', isActive: true }, + { id: 7, firstName: 'Grace', lastName: 'Lee', email: 'grace@test.com', isActive: true }, + { id: 8, firstName: 'Henry', lastName: 'Taylor', email: 'henry@test.com', isActive: true }, + { id: 9, firstName: 'Ivy', lastName: 'Chen', email: 'ivy@test.com', isActive: true }, + { id: 10, firstName: 'Jack', lastName: 'Davis', email: 'jack@test.com', isActive: true }, + { id: 11, firstName: 'Kate', lastName: 'Moore', email: 'kate@test.com', isActive: true }, + { id: 12, firstName: 'Leo', lastName: 'Garcia', email: 'leo@test.com', isActive: true } + ] + } + + // Transform API response into display format (matching doc example) + const processApiResponse = pipe( + // Extract data from response + response => response.data, + + // Filter active users only + users => users.filter(u => u.isActive), + + // Sort by name (using lastName for sorting) + users => users.sort((a, b) => a.firstName.localeCompare(b.firstName)), + + // Transform to display format + users => users.map(u => ({ + id: u.id, + displayName: `${u.firstName} ${u.lastName}`, + email: u.email + })), + + // Take first 10 + users => users.slice(0, 10) + ) + + const result = processApiResponse(apiResponse) + + // Verify pipeline worked correctly + expect(result).toHaveLength(10) + + // Alice was filtered out (isActive: false) + expect(result.find(u => u.displayName === 'Alice Smith')).toBeUndefined() + + // First user should be Bob (alphabetically first among active users) + expect(result[0].displayName).toBe('Bob Jones') + + // Verify display format + expect(result[0]).toHaveProperty('id') + expect(result[0]).toHaveProperty('displayName') + expect(result[0]).toHaveProperty('email') + + // Verify sorting (alphabetical by firstName) + const names = result.map(u => u.displayName.split(' ')[0]) + const sortedNames = [...names].sort() + expect(names).toEqual(sortedNames) + }) + }) + + describe('compose() Direction Example (doc lines 658-664)', () => { + it('should process right-to-left with getName/toUpperCase/addExclaim', () => { + const getName = obj => obj.name + const toUpperCase = str => str.toUpperCase() + const addExclaim = str => str + '!' + + // compose processes right-to-left + const shout = compose(addExclaim, toUpperCase, getName) + + expect(shout({ name: 'alice' })).toBe('ALICE!') + + // This is equivalent to nested calls: + const manualResult = addExclaim(toUpperCase(getName({ name: 'alice' }))) + expect(shout({ name: 'alice' })).toBe(manualResult) + }) + }) + + describe('pipe/compose Equivalence (doc lines 669-672)', () => { + it('should produce same result: pipe(a, b, c)(x) === compose(c, b, a)(x)', () => { + const a = x => x + 1 + const b = x => x * 2 + const c = x => x - 3 + + const input = 10 + + // pipe: a first, then b, then c + const pipedResult = pipe(a, b, c)(input) + + // compose: c(b(a(x))) - reversed argument order + const composedResult = compose(c, b, a)(input) + + expect(pipedResult).toBe(composedResult) + + // Verify the actual value: (10 + 1) * 2 - 3 = 19 + expect(pipedResult).toBe(19) + }) + + it('should demonstrate both directions with same functions', () => { + const add5 = x => x + 5 + const double = x => x * 2 + const square = x => x * x + + const input = 3 + + // pipe(add5, double, square)(3) = ((3 + 5) * 2)² = 256 + expect(pipe(add5, double, square)(input)).toBe(256) + + // compose(add5, double, square)(3) = (3² * 2) + 5 = 23 + expect(compose(add5, double, square)(input)).toBe(23) + + // To get same result with compose, reverse the order + expect(compose(square, double, add5)(input)).toBe(256) + }) + }) + + describe('Why Multi-Argument Functions Do Not Compose (doc lines 769-775)', () => { + it('should demonstrate NaN problem with non-curried functions', () => { + const add = (a, b) => a + b + const multiply = (a, b) => a * b + + // This doesn't work as expected! + const addThenMultiply = pipe(add, multiply) + + // When called: add(1, 2) returns 3 + // Then multiply(3) is called with only one argument + // multiply(3, undefined) = 3 * undefined = NaN + const result = addThenMultiply(1, 2) + + expect(result).toBeNaN() + }) + + it('should work correctly with curried versions', () => { + // Curried versions + const add = a => b => a + b + const multiply = a => b => a * b + + // Now we can compose! + const add5 = add(5) // x => 5 + x + const double = multiply(2) // x => 2 * x + + const add5ThenDouble = pipe(add5, double) + + // (10 + 5) * 2 = 30 + expect(add5ThenDouble(10)).toBe(30) + }) + }) + + describe('Data-First vs Data-Last Argument Order (doc lines 984-994)', () => { + it('should show data-first makes composition harder', () => { + // Data-first: hard to compose + const multiplyFirst = (value, factor) => value * factor + + // Can't easily create a reusable "double" function for pipelines + // Would need to wrap it: + const doubleFirst = value => multiplyFirst(value, 2) + const tripleFirst = value => multiplyFirst(value, 3) + + // Works, but requires manual wrapping each time + expect(pipe(doubleFirst, tripleFirst)(5)).toBe(30) + }) + + it('should show data-last composes naturally', () => { + // Data-last: composes well + const multiply = factor => value => value * factor + + const double = multiply(2) + const triple = multiply(3) + + // Composes naturally without any wrapping + expect(pipe(double, triple)(5)).toBe(30) + + // Can easily create new specialized functions + const quadruple = multiply(4) + expect(pipe(double, quadruple)(5)).toBe(40) + }) + }) + + describe('Manual Composition with Nested Calls (doc lines 526-538)', () => { + it('should work with nested function calls', () => { + const add10 = x => x + 10 + const multiply2 = x => x * 2 + const subtract5 = x => x - 5 + + // Manual composition (nested calls) + // Step by step: 5 → 15 → 30 → 25 + const result = subtract5(multiply2(add10(5))) + + expect(result).toBe(25) + }) + + it('should produce same result with compose function', () => { + const add10 = x => x + 10 + const multiply2 = x => x * 2 + const subtract5 = x => x - 5 + + // With a compose function + const composed = compose(subtract5, multiply2, add10) + + expect(composed(5)).toBe(25) + + // Verify it matches manual nesting + const manual = subtract5(multiply2(add10(5))) + expect(composed(5)).toBe(manual) + }) + + it('should be more readable with pipe', () => { + const add10 = x => x + 10 + const multiply2 = x => x * 2 + const subtract5 = x => x - 5 + + // With pipe (reads in execution order) + const piped = pipe(add10, multiply2, subtract5) + + expect(piped(5)).toBe(25) + }) + }) + + describe('Opening Example from Documentation (doc lines 9-20)', () => { + it('should demonstrate the opening currying example', () => { + // Currying: one argument at a time + const add = a => b => c => a + b + c + expect(add(1)(2)(3)).toBe(6) + }) + + it('should demonstrate the opening composition example', () => { + const getName = obj => obj.name + const trim = str => str.trim() + const capitalize = str => str.charAt(0).toUpperCase() + str.slice(1).toLowerCase() + + // Composition: chain functions together + const process = pipe( + getName, + trim, + capitalize + ) + + expect(process({ name: " alice " })).toBe("Alice") + }) + }) + }) +}) diff --git a/tests/functional-programming/higher-order-functions/higher-order-functions.test.js b/tests/functional-programming/higher-order-functions/higher-order-functions.test.js new file mode 100644 index 00000000..56827fe7 --- /dev/null +++ b/tests/functional-programming/higher-order-functions/higher-order-functions.test.js @@ -0,0 +1,516 @@ +import { describe, it, expect, vi } from 'vitest' + +describe('Higher-Order Functions', () => { + describe('Functions that accept functions as arguments', () => { + it('should execute the passed function', () => { + const mockFn = vi.fn() + + function doTwice(action) { + action() + action() + } + + doTwice(mockFn) + + expect(mockFn).toHaveBeenCalledTimes(2) + }) + + it('should repeat an action n times', () => { + const results = [] + + function repeat(times, action) { + for (let i = 0; i < times; i++) { + action(i) + } + } + + repeat(5, i => results.push(i)) + + expect(results).toEqual([0, 1, 2, 3, 4]) + }) + + it('should apply different logic with the same structure', () => { + function calculate(numbers, operation) { + const result = [] + for (const num of numbers) { + result.push(operation(num)) + } + return result + } + + const numbers = [1, 2, 3, 4, 5] + + const doubled = calculate(numbers, n => n * 2) + const squared = calculate(numbers, n => n * n) + const incremented = calculate(numbers, n => n + 1) + + expect(doubled).toEqual([2, 4, 6, 8, 10]) + expect(squared).toEqual([1, 4, 9, 16, 25]) + expect(incremented).toEqual([2, 3, 4, 5, 6]) + }) + + it('should implement unless as a control flow abstraction', () => { + const results = [] + + function unless(condition, action) { + if (!condition) { + action() + } + } + + for (let i = 0; i < 5; i++) { + unless(i % 2 === 1, () => results.push(i)) + } + + expect(results).toEqual([0, 2, 4]) + }) + + it('should calculate circle properties using formulas', () => { + function calculate(radii, formula) { + const result = [] + for (const radius of radii) { + result.push(formula(radius)) + } + return result + } + + const area = r => Math.PI * r * r + const circumference = r => 2 * Math.PI * r + const diameter = r => 2 * r + const volume = r => (4/3) * Math.PI * r * r * r + + const radii = [1, 2, 3] + + // Test area: π * r² + const areas = calculate(radii, area) + expect(areas[0]).toBeCloseTo(Math.PI, 5) // π * 1² = π + expect(areas[1]).toBeCloseTo(4 * Math.PI, 5) // π * 2² = 4π + expect(areas[2]).toBeCloseTo(9 * Math.PI, 5) // π * 3² = 9π + + // Test circumference: 2πr + const circumferences = calculate(radii, circumference) + expect(circumferences[0]).toBeCloseTo(2 * Math.PI, 5) // 2π * 1 + expect(circumferences[1]).toBeCloseTo(4 * Math.PI, 5) // 2π * 2 + expect(circumferences[2]).toBeCloseTo(6 * Math.PI, 5) // 2π * 3 + + // Test diameter: 2r + const diameters = calculate(radii, diameter) + expect(diameters).toEqual([2, 4, 6]) + + // Test volume: (4/3)πr³ + const volumes = calculate(radii, volume) + expect(volumes[0]).toBeCloseTo((4/3) * Math.PI, 5) // (4/3)π * 1³ + expect(volumes[1]).toBeCloseTo((4/3) * Math.PI * 8, 5) // (4/3)π * 2³ + expect(volumes[2]).toBeCloseTo((4/3) * Math.PI * 27, 5) // (4/3)π * 3³ + }) + }) + + describe('Functions that return functions', () => { + it('should create a greaterThan comparator', () => { + function greaterThan(n) { + return function(m) { + return m > n + } + } + + const greaterThan10 = greaterThan(10) + const greaterThan100 = greaterThan(100) + + expect(greaterThan10(11)).toBe(true) + expect(greaterThan10(5)).toBe(false) + expect(greaterThan10(10)).toBe(false) + expect(greaterThan100(150)).toBe(true) + expect(greaterThan100(50)).toBe(false) + }) + + it('should create multiplier functions', () => { + function multiplier(factor) { + return number => number * factor + } + + const double = multiplier(2) + const triple = multiplier(3) + const tenX = multiplier(10) + + expect(double(5)).toBe(10) + expect(triple(5)).toBe(15) + expect(tenX(5)).toBe(50) + expect(double(0)).toBe(0) + expect(triple(-3)).toBe(-9) + }) + + it('should wrap functions with logging behavior', () => { + const logs = [] + + function noisy(fn) { + return function(...args) { + logs.push({ type: 'call', args }) + const result = fn(...args) + logs.push({ type: 'return', result }) + return result + } + } + + const noisyMax = noisy(Math.max) + const result = noisyMax(3, 1, 4, 1, 5) + + expect(result).toBe(5) + expect(logs).toEqual([ + { type: 'call', args: [3, 1, 4, 1, 5] }, + { type: 'return', result: 5 } + ]) + }) + + it('should wrap Math.floor with noisy', () => { + const logs = [] + + function noisy(fn) { + return function(...args) { + logs.push({ type: 'call', args }) + const result = fn(...args) + logs.push({ type: 'return', result }) + return result + } + } + + const noisyFloor = noisy(Math.floor) + const result = noisyFloor(4.7) + + expect(result).toBe(4) + expect(logs).toEqual([ + { type: 'call', args: [4.7] }, + { type: 'return', result: 4 } + ]) + }) + + it('should create greeting functions with createGreeter', () => { + function createGreeter(greeting) { + return function(name) { + return `${greeting}, ${name}!` + } + } + + const sayHello = createGreeter('Hello') + const sayGoodbye = createGreeter('Goodbye') + + expect(sayHello('Alice')).toBe('Hello, Alice!') + expect(sayHello('Bob')).toBe('Hello, Bob!') + expect(sayGoodbye('Alice')).toBe('Goodbye, Alice!') + }) + + it('should allow direct factory invocation', () => { + function multiplier(factor) { + return number => number * factor + } + + // Direct invocation without storing intermediate function + expect(multiplier(7)(3)).toBe(21) + expect(multiplier(2)(10)).toBe(20) + expect(multiplier(0.5)(100)).toBe(50) + }) + }) + + describe('Function factories', () => { + it('should create validator functions', () => { + function createValidator(min, max) { + return function(value) { + return value >= min && value <= max + } + } + + const isValidAge = createValidator(0, 120) + const isValidPercentage = createValidator(0, 100) + + expect(isValidAge(25)).toBe(true) + expect(isValidAge(150)).toBe(false) + expect(isValidAge(-5)).toBe(false) + expect(isValidPercentage(50)).toBe(true) + expect(isValidPercentage(101)).toBe(false) + }) + + it('should create formatter functions', () => { + function createFormatter(prefix, suffix) { + return function(value) { + return `${prefix}${value}${suffix}` + } + } + + const formatDollars = createFormatter('$', '') + const formatPercent = createFormatter('', '%') + const formatParens = createFormatter('(', ')') + + expect(formatDollars(99.99)).toBe('$99.99') + expect(formatPercent(75)).toBe('75%') + expect(formatParens('aside')).toBe('(aside)') + }) + + it('should implement partial application', () => { + function partial(fn, ...presetArgs) { + return function(...laterArgs) { + return fn(...presetArgs, ...laterArgs) + } + } + + function greet(greeting, punctuation, name) { + return `${greeting}, ${name}${punctuation}` + } + + const sayHello = partial(greet, 'Hello', '!') + const askHowAreYou = partial(greet, 'How are you', '?') + + expect(sayHello('Alice')).toBe('Hello, Alice!') + expect(sayHello('Bob')).toBe('Hello, Bob!') + expect(askHowAreYou('Charlie')).toBe('How are you, Charlie?') + }) + + it('should create rating validator', () => { + function createValidator(min, max) { + return function(value) { + return value >= min && value <= max + } + } + + // Rating from 1 to 5 stars + const isValidRating = createValidator(1, 5) + + expect(isValidRating(3)).toBe(true) + expect(isValidRating(1)).toBe(true) // At min + expect(isValidRating(5)).toBe(true) // At max + expect(isValidRating(0)).toBe(false) // Below min + expect(isValidRating(6)).toBe(false) // Above max + }) + }) + + describe('Closures with higher-order functions', () => { + it('should create independent counters', () => { + function createCounter(start = 0) { + let count = start + return function() { + count++ + return count + } + } + + const counter1 = createCounter() + const counter2 = createCounter(100) + + expect(counter1()).toBe(1) + expect(counter1()).toBe(2) + expect(counter1()).toBe(3) + + expect(counter2()).toBe(101) + expect(counter2()).toBe(102) + + // counter1 should not be affected by counter2 + expect(counter1()).toBe(4) + }) + + it('should create private state with closures', () => { + function createBankAccount(initialBalance) { + let balance = initialBalance + + return { + deposit(amount) { + if (amount > 0) { + balance += amount + return balance + } + return balance + }, + withdraw(amount) { + if (amount > 0 && amount <= balance) { + balance -= amount + return balance + } + return 'Insufficient funds' + }, + getBalance() { + return balance + } + } + } + + const account = createBankAccount(100) + + expect(account.getBalance()).toBe(100) + expect(account.deposit(50)).toBe(150) + expect(account.withdraw(30)).toBe(120) + expect(account.withdraw(200)).toBe('Insufficient funds') + expect(account.getBalance()).toBe(120) + + // balance is not directly accessible + expect(account.balance).toBeUndefined() + }) + }) + + describe('Common mistakes', () => { + it('should demonstrate the parseInt gotcha with map', () => { + // This is the WRONG way - demonstrates the bug + const buggyResult = ['1', '2', '3'].map(parseInt) + + // parseInt receives (string, index) from map + // parseInt('1', 0) → 1 (radix 0 is treated as 10) + // parseInt('2', 1) → NaN (radix 1 is invalid) + // parseInt('3', 2) → NaN (3 is not valid in binary) + expect(buggyResult).toEqual([1, NaN, NaN]) + + // The CORRECT way + const correctResult = ['1', '2', '3'].map(str => parseInt(str, 10)) + expect(correctResult).toEqual([1, 2, 3]) + + // Alternative correct way using Number + const alternativeResult = ['1', '2', '3'].map(Number) + expect(alternativeResult).toEqual([1, 2, 3]) + }) + + it('should demonstrate losing this context', () => { + const user = { + name: 'Alice', + greet() { + // Using optional chaining to handle undefined 'this' safely + return `Hello, I'm ${this?.name ?? 'undefined'}` + } + } + + // Direct call works + expect(user.greet()).toBe("Hello, I'm Alice") + + // Passing as callback loses 'this' + function callLater(fn) { + return fn() + } + + // This fails because 'this' is lost (undefined in strict mode) + const lostThis = callLater(user.greet) + expect(lostThis).toBe("Hello, I'm undefined") + + // Fix with bind + const boundGreet = callLater(user.greet.bind(user)) + expect(boundGreet).toBe("Hello, I'm Alice") + + // Fix with arrow function wrapper + const wrappedGreet = callLater(() => user.greet()) + expect(wrappedGreet).toBe("Hello, I'm Alice") + }) + + it('should show difference between map and forEach return values', () => { + const numbers = [1, 2, 3] + + // map returns a new array + const mapResult = numbers.map(n => n * 2) + expect(mapResult).toEqual([2, 4, 6]) + + // forEach returns undefined + const forEachResult = numbers.forEach(n => n * 2) + expect(forEachResult).toBeUndefined() + }) + }) + + describe('First-class functions', () => { + it('should allow assigning functions to variables', () => { + const greet = function(name) { + return `Hello, ${name}!` + } + + const add = (a, b) => a + b + + expect(greet('Alice')).toBe('Hello, Alice!') + expect(add(2, 3)).toBe(5) + }) + + it('should allow passing functions as arguments', () => { + function callWith5(fn) { + return fn(5) + } + + expect(callWith5(n => n * 2)).toBe(10) + expect(callWith5(n => n + 3)).toBe(8) + expect(callWith5(Math.sqrt)).toBeCloseTo(2.236, 2) + }) + + it('should allow returning functions from functions', () => { + function createAdder(x) { + return function(y) { + return x + y + } + } + + const add5 = createAdder(5) + const add10 = createAdder(10) + + expect(add5(3)).toBe(8) + expect(add10(3)).toBe(13) + }) + }) + + describe('Built-in higher-order functions (overview)', () => { + const numbers = [1, 2, 3, 4, 5] + + it('should use forEach for side effects', () => { + const results = [] + const returnValue = numbers.forEach(n => results.push(n * 2)) + + expect(results).toEqual([2, 4, 6, 8, 10]) + expect(returnValue).toBeUndefined() + }) + + it('should use map for transformations', () => { + const doubled = numbers.map(n => n * 2) + + expect(doubled).toEqual([2, 4, 6, 8, 10]) + expect(numbers).toEqual([1, 2, 3, 4, 5]) // Original unchanged + }) + + it('should use filter for selection', () => { + const evens = numbers.filter(n => n % 2 === 0) + const greaterThan3 = numbers.filter(n => n > 3) + + expect(evens).toEqual([2, 4]) + expect(greaterThan3).toEqual([4, 5]) + }) + + it('should use reduce for accumulation', () => { + const sum = numbers.reduce((acc, n) => acc + n, 0) + const product = numbers.reduce((acc, n) => acc * n, 1) + + expect(sum).toBe(15) + expect(product).toBe(120) + }) + + it('should use find to get first matching element', () => { + const firstEven = numbers.find(n => n % 2 === 0) + const firstGreaterThan10 = numbers.find(n => n > 10) + + expect(firstEven).toBe(2) + expect(firstGreaterThan10).toBeUndefined() + }) + + it('should use some to test if any element matches', () => { + const hasEven = numbers.some(n => n % 2 === 0) + const hasNegative = numbers.some(n => n < 0) + + expect(hasEven).toBe(true) + expect(hasNegative).toBe(false) + }) + + it('should use every to test if all elements match', () => { + const allPositive = numbers.every(n => n > 0) + const allEven = numbers.every(n => n % 2 === 0) + + expect(allPositive).toBe(true) + expect(allEven).toBe(false) + }) + + it('should use sort with a comparator function', () => { + const unsorted = [3, 1, 4, 1, 5, 9, 2, 6] + + // Ascending order + const ascending = [...unsorted].sort((a, b) => a - b) + expect(ascending).toEqual([1, 1, 2, 3, 4, 5, 6, 9]) + + // Descending order + const descending = [...unsorted].sort((a, b) => b - a) + expect(descending).toEqual([9, 6, 5, 4, 3, 2, 1, 1]) + }) + }) +}) diff --git a/tests/functional-programming/map-reduce-filter/map-reduce-filter.test.js b/tests/functional-programming/map-reduce-filter/map-reduce-filter.test.js new file mode 100644 index 00000000..eb0a675e --- /dev/null +++ b/tests/functional-programming/map-reduce-filter/map-reduce-filter.test.js @@ -0,0 +1,779 @@ +import { describe, it, expect } from 'vitest' + +describe('map, reduce, filter', () => { + + describe('map()', () => { + it('should transform every element in the array', () => { + const numbers = [1, 2, 3, 4] + const doubled = numbers.map(n => n * 2) + + expect(doubled).toEqual([2, 4, 6, 8]) + }) + + it('should not mutate the original array', () => { + const original = [1, 2, 3] + const mapped = original.map(n => n * 10) + + expect(original).toEqual([1, 2, 3]) + expect(mapped).toEqual([10, 20, 30]) + }) + + it('should pass element, index, and array to callback', () => { + const letters = ['a', 'b', 'c'] + const result = letters.map((letter, index, arr) => ({ + letter, + index, + arrayLength: arr.length + })) + + expect(result).toEqual([ + { letter: 'a', index: 0, arrayLength: 3 }, + { letter: 'b', index: 1, arrayLength: 3 }, + { letter: 'c', index: 2, arrayLength: 3 } + ]) + }) + + it('should return undefined for elements when callback has no return', () => { + const numbers = [1, 2, 3] + const result = numbers.map(n => { + n * 2 // No return statement + }) + + expect(result).toEqual([undefined, undefined, undefined]) + }) + + it('demonstrates the parseInt pitfall', () => { + const strings = ['1', '2', '3'] + + // The pitfall: parseInt receives (element, index, array) + // So it becomes parseInt('1', 0), parseInt('2', 1), parseInt('3', 2) + const wrongResult = strings.map(parseInt) + expect(wrongResult).toEqual([1, NaN, NaN]) + + // The fix: wrap in arrow function or use Number + const correctResult1 = strings.map(str => parseInt(str, 10)) + expect(correctResult1).toEqual([1, 2, 3]) + + const correctResult2 = strings.map(Number) + expect(correctResult2).toEqual([1, 2, 3]) + }) + + it('should extract properties from objects', () => { + const users = [ + { id: 1, name: 'Alice' }, + { id: 2, name: 'Bob' } + ] + + const names = users.map(user => user.name) + expect(names).toEqual(['Alice', 'Bob']) + }) + + it('should transform object shapes', () => { + const users = [ + { firstName: 'Alice', lastName: 'Smith' }, + { firstName: 'Bob', lastName: 'Jones' } + ] + + const fullNames = users.map(user => ({ + fullName: `${user.firstName} ${user.lastName}` + })) + + expect(fullNames).toEqual([ + { fullName: 'Alice Smith' }, + { fullName: 'Bob Jones' } + ]) + }) + + it('should convert strings to uppercase', () => { + const words = ['hello', 'world'] + const shouting = words.map(word => word.toUpperCase()) + + expect(shouting).toEqual(['HELLO', 'WORLD']) + }) + + it('should square each number', () => { + const numbers = [1, 2, 3, 4, 5] + const squares = numbers.map(n => n * n) + + expect(squares).toEqual([1, 4, 9, 16, 25]) + }) + + it('should add index prefix to each letter', () => { + const letters = ['a', 'b', 'c', 'd'] + const indexed = letters.map((letter, index) => `${index}: ${letter}`) + + expect(indexed).toEqual(['0: a', '1: b', '2: c', '3: d']) + }) + + it('should create objects with sequential IDs from items', () => { + const items = ['apple', 'banana', 'cherry'] + const products = items.map((name, index) => ({ + id: index + 1, + name + })) + + expect(products).toEqual([ + { id: 1, name: 'apple' }, + { id: 2, name: 'banana' }, + { id: 3, name: 'cherry' } + ]) + }) + }) + + describe('filter()', () => { + it('should keep elements that pass the test', () => { + const numbers = [1, 2, 3, 4, 5, 6] + const evens = numbers.filter(n => n % 2 === 0) + + expect(evens).toEqual([2, 4, 6]) + }) + + it('should return empty array when no elements match', () => { + const numbers = [1, 3, 5, 7] + const evens = numbers.filter(n => n % 2 === 0) + + expect(evens).toEqual([]) + }) + + it('should not mutate the original array', () => { + const original = [1, 2, 3, 4, 5] + const filtered = original.filter(n => n > 3) + + expect(original).toEqual([1, 2, 3, 4, 5]) + expect(filtered).toEqual([4, 5]) + }) + + it('should evaluate truthy/falsy values correctly', () => { + const mixed = [0, 1, '', 'hello', null, undefined, false, true] + const truthy = mixed.filter(Boolean) + + expect(truthy).toEqual([1, 'hello', true]) + }) + + it('should filter objects by property', () => { + const users = [ + { name: 'Alice', active: true }, + { name: 'Bob', active: false }, + { name: 'Charlie', active: true } + ] + + const activeUsers = users.filter(user => user.active) + + expect(activeUsers).toEqual([ + { name: 'Alice', active: true }, + { name: 'Charlie', active: true } + ]) + }) + + it('demonstrates filter vs find', () => { + const numbers = [1, 2, 3, 4, 5, 6] + + // filter returns ALL matches as an array + const allEvens = numbers.filter(n => n % 2 === 0) + expect(allEvens).toEqual([2, 4, 6]) + + // find returns the FIRST match (not an array) + const firstEven = numbers.find(n => n % 2 === 0) + expect(firstEven).toBe(2) + }) + + it('should support multiple conditions', () => { + const products = [ + { name: 'Laptop', price: 1000, inStock: true }, + { name: 'Phone', price: 500, inStock: false }, + { name: 'Tablet', price: 300, inStock: true } + ] + + const affordableInStock = products.filter( + p => p.inStock && p.price < 500 + ) + + expect(affordableInStock).toEqual([ + { name: 'Tablet', price: 300, inStock: true } + ]) + }) + + it('should keep only odd numbers', () => { + const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + const odds = numbers.filter(n => n % 2 !== 0) + + expect(odds).toEqual([1, 3, 5, 7, 9]) + }) + + it('should keep numbers greater than threshold', () => { + const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + const big = numbers.filter(n => n > 5) + + expect(big).toEqual([6, 7, 8, 9, 10]) + }) + + it('should keep numbers in a range', () => { + const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + const middle = numbers.filter(n => n >= 3 && n <= 7) + + expect(middle).toEqual([3, 4, 5, 6, 7]) + }) + + it('should search products by name case-insensitively', () => { + const products = [ + { name: 'MacBook Pro', category: 'laptops', price: 2000 }, + { name: 'iPhone', category: 'phones', price: 1000 }, + { name: 'iPad', category: 'tablets', price: 800 }, + { name: 'Dell XPS', category: 'laptops', price: 1500 } + ] + + const searchTerm = 'mac' + const results = products.filter(p => + p.name.toLowerCase().includes(searchTerm.toLowerCase()) + ) + + expect(results).toEqual([ + { name: 'MacBook Pro', category: 'laptops', price: 2000 } + ]) + }) + + it('should filter products by category', () => { + const products = [ + { name: 'MacBook Pro', category: 'laptops', price: 2000 }, + { name: 'iPhone', category: 'phones', price: 1000 }, + { name: 'iPad', category: 'tablets', price: 800 }, + { name: 'Dell XPS', category: 'laptops', price: 1500 } + ] + + const laptops = products.filter(p => p.category === 'laptops') + + expect(laptops).toEqual([ + { name: 'MacBook Pro', category: 'laptops', price: 2000 }, + { name: 'Dell XPS', category: 'laptops', price: 1500 } + ]) + }) + + it('should filter products by price range', () => { + const products = [ + { name: 'MacBook Pro', category: 'laptops', price: 2000 }, + { name: 'iPhone', category: 'phones', price: 1000 }, + { name: 'iPad', category: 'tablets', price: 800 }, + { name: 'Dell XPS', category: 'laptops', price: 1500 } + ] + + const affordable = products.filter(p => p.price <= 1000) + + expect(affordable).toEqual([ + { name: 'iPhone', category: 'phones', price: 1000 }, + { name: 'iPad', category: 'tablets', price: 800 } + ]) + }) + }) + + describe('reduce()', () => { + it('should combine array elements into a single value', () => { + const numbers = [1, 2, 3, 4, 5] + const sum = numbers.reduce((acc, n) => acc + n, 0) + + expect(sum).toBe(15) + }) + + it('should use initial value as starting accumulator', () => { + const numbers = [1, 2, 3] + const sumStartingAt10 = numbers.reduce((acc, n) => acc + n, 10) + + expect(sumStartingAt10).toBe(16) // 10 + 1 + 2 + 3 + }) + + it('should throw on empty array without initial value', () => { + const empty = [] + + expect(() => { + empty.reduce((acc, n) => acc + n) + }).toThrow(TypeError) + }) + + it('should return initial value for empty array', () => { + const empty = [] + const result = empty.reduce((acc, n) => acc + n, 0) + + expect(result).toBe(0) + }) + + it('should count occurrences', () => { + const fruits = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple'] + + const count = fruits.reduce((acc, fruit) => { + acc[fruit] = (acc[fruit] || 0) + 1 + return acc + }, {}) + + expect(count).toEqual({ + apple: 3, + banana: 2, + orange: 1 + }) + }) + + it('should group by property', () => { + const people = [ + { name: 'Alice', department: 'Engineering' }, + { name: 'Bob', department: 'Marketing' }, + { name: 'Charlie', department: 'Engineering' } + ] + + const byDepartment = people.reduce((acc, person) => { + const dept = person.department + if (!acc[dept]) { + acc[dept] = [] + } + acc[dept].push(person.name) + return acc + }, {}) + + expect(byDepartment).toEqual({ + Engineering: ['Alice', 'Charlie'], + Marketing: ['Bob'] + }) + }) + + it('should build objects from arrays', () => { + const pairs = [['a', 1], ['b', 2], ['c', 3]] + + const obj = pairs.reduce((acc, [key, value]) => { + acc[key] = value + return acc + }, {}) + + expect(obj).toEqual({ a: 1, b: 2, c: 3 }) + }) + + it('can implement map with reduce', () => { + const numbers = [1, 2, 3, 4] + + const doubled = numbers.reduce((acc, n) => { + acc.push(n * 2) + return acc + }, []) + + expect(doubled).toEqual([2, 4, 6, 8]) + }) + + it('can implement filter with reduce', () => { + const numbers = [1, 2, 3, 4, 5, 6] + + const evens = numbers.reduce((acc, n) => { + if (n % 2 === 0) { + acc.push(n) + } + return acc + }, []) + + expect(evens).toEqual([2, 4, 6]) + }) + + it('should calculate average', () => { + const numbers = [10, 20, 30, 40, 50] + + const sum = numbers.reduce((acc, n) => acc + n, 0) + const average = sum / numbers.length + + expect(average).toBe(30) + }) + + it('should find max value', () => { + const numbers = [5, 2, 9, 1, 7] + + const max = numbers.reduce((acc, n) => n > acc ? n : acc, numbers[0]) + + expect(max).toBe(9) + }) + + it('should find minimum value', () => { + const numbers = [5, 2, 9, 1, 7] + + const min = numbers.reduce((acc, n) => n < acc ? n : acc, numbers[0]) + + expect(min).toBe(1) + }) + + it('should flatten nested arrays with reduce', () => { + const nested = [[1, 2], [3, 4], [5, 6]] + + const flat = nested.reduce((acc, arr) => acc.concat(arr), []) + + expect(flat).toEqual([1, 2, 3, 4, 5, 6]) + }) + + it('should implement myMap using reduce inline', () => { + const array = [1, 2, 3] + const callback = n => n * 2 + + // myMap implementation from concept page + const result = array.reduce((acc, element, index) => { + acc.push(callback(element, index, array)) + return acc + }, []) + + expect(result).toEqual([2, 4, 6]) + }) + }) + + describe('Method Chaining', () => { + it('should chain filter → map → reduce', () => { + const products = [ + { name: 'Laptop', price: 1000, inStock: true }, + { name: 'Phone', price: 500, inStock: false }, + { name: 'Tablet', price: 300, inStock: true }, + { name: 'Watch', price: 200, inStock: true } + ] + + const totalInStock = products + .filter(p => p.inStock) + .map(p => p.price) + .reduce((sum, price) => sum + price, 0) + + expect(totalInStock).toBe(1500) + }) + + it('demonstrates real-world data pipeline', () => { + const transactions = [ + { type: 'sale', amount: 100 }, + { type: 'refund', amount: 30 }, + { type: 'sale', amount: 200 }, + { type: 'sale', amount: 150 }, + { type: 'refund', amount: 50 } + ] + + // Calculate total sales (not refunds) + const totalSales = transactions + .filter(t => t.type === 'sale') + .map(t => t.amount) + .reduce((sum, amount) => sum + amount, 0) + + expect(totalSales).toBe(450) + + // Calculate net (sales - refunds) + const net = transactions.reduce((acc, t) => { + return t.type === 'sale' + ? acc + t.amount + : acc - t.amount + }, 0) + + expect(net).toBe(370) // 450 - 80 + }) + + it('should get active premium users emails', () => { + const users = [ + { email: 'alice@example.com', active: true, plan: 'premium' }, + { email: 'bob@example.com', active: false, plan: 'premium' }, + { email: 'charlie@example.com', active: true, plan: 'free' }, + { email: 'diana@example.com', active: true, plan: 'premium' } + ] + + const premiumEmails = users + .filter(u => u.active) + .filter(u => u.plan === 'premium') + .map(u => u.email) + + expect(premiumEmails).toEqual([ + 'alice@example.com', + 'diana@example.com' + ]) + }) + + it('should calculate cart total with discounts', () => { + const cart = [ + { name: 'Laptop', price: 1000, quantity: 1, discountPercent: 10 }, + { name: 'Mouse', price: 50, quantity: 2, discountPercent: 0 }, + { name: 'Keyboard', price: 100, quantity: 1, discountPercent: 20 } + ] + + const total = cart + .map(item => { + const subtotal = item.price * item.quantity + const discount = subtotal * (item.discountPercent / 100) + return subtotal - discount + }) + .reduce((sum, price) => sum + price, 0) + + // Laptop: 1000 * 1 - 10% = 900 + // Mouse: 50 * 2 - 0% = 100 + // Keyboard: 100 * 1 - 20% = 80 + // Total: 900 + 100 + 80 = 1080 + expect(total).toBe(1080) + }) + + it('should get top 3 performers sorted by sales', () => { + const salespeople = [ + { name: 'Alice', sales: 50000 }, + { name: 'Bob', sales: 75000 }, + { name: 'Charlie', sales: 45000 }, + { name: 'Diana', sales: 90000 }, + { name: 'Eve', sales: 60000 } + ] + + const top3 = salespeople + .filter(p => p.sales >= 50000) + .sort((a, b) => b.sales - a.sales) + .slice(0, 3) + .map(p => p.name) + + expect(top3).toEqual(['Diana', 'Bob', 'Eve']) + }) + }) + + describe('Other Array Methods', () => { + it('find() returns first matching element', () => { + const users = [ + { id: 1, name: 'Alice' }, + { id: 2, name: 'Bob' }, + { id: 3, name: 'Charlie' } + ] + + const bob = users.find(u => u.id === 2) + expect(bob).toEqual({ id: 2, name: 'Bob' }) + + const notFound = users.find(u => u.id === 999) + expect(notFound).toBeUndefined() + }) + + it('some() returns true if any element matches', () => { + const numbers = [1, 2, 3, 4, 5] + + expect(numbers.some(n => n > 4)).toBe(true) + expect(numbers.some(n => n > 10)).toBe(false) + }) + + it('every() returns true if all elements match', () => { + const numbers = [2, 4, 6, 8] + + expect(numbers.every(n => n % 2 === 0)).toBe(true) + expect(numbers.every(n => n > 5)).toBe(false) + }) + + it('includes() checks for value membership', () => { + const fruits = ['apple', 'banana', 'orange'] + + expect(fruits.includes('banana')).toBe(true) + expect(fruits.includes('grape')).toBe(false) + }) + + it('findIndex() returns index of first match', () => { + const users = [ + { id: 1, name: 'Alice' }, + { id: 2, name: 'Bob' } + ] + + const bobIndex = users.findIndex(u => u.name === 'Bob') + expect(bobIndex).toBe(1) + + const notFoundIndex = users.findIndex(u => u.name === 'Eve') + expect(notFoundIndex).toBe(-1) + }) + + it('flat() flattens nested arrays', () => { + const nested = [[1, 2], [3, 4], [5, 6]] + expect(nested.flat()).toEqual([1, 2, 3, 4, 5, 6]) + + const deepNested = [1, [2, [3, [4]]]] + expect(deepNested.flat(2)).toEqual([1, 2, 3, [4]]) + expect(deepNested.flat(Infinity)).toEqual([1, 2, 3, 4]) + }) + + it('flatMap() maps then flattens', () => { + const sentences = ['hello world', 'foo bar'] + const words = sentences.flatMap(s => s.split(' ')) + + expect(words).toEqual(['hello', 'world', 'foo', 'bar']) + }) + }) + + describe('Common Mistakes', () => { + it('demonstrates mutation issue in map', () => { + const users = [ + { name: 'Alice', score: 85 }, + { name: 'Bob', score: 92 } + ] + + // ❌ WRONG: This mutates the original objects + const mutated = users.map(user => { + user.score += 5 // Mutates original! + return user + }) + + expect(users[0].score).toBe(90) // Original was mutated! + + // Reset for next test + const users2 = [ + { name: 'Alice', score: 85 }, + { name: 'Bob', score: 92 } + ] + + // ✓ CORRECT: Create new objects + const notMutated = users2.map(user => ({ + ...user, + score: user.score + 5 + })) + + expect(users2[0].score).toBe(85) // Original unchanged + expect(notMutated[0].score).toBe(90) + }) + + it('demonstrates reduce without initial value type issues', () => { + const products = [ + { name: 'Laptop', price: 1000 }, + { name: 'Phone', price: 500 } + ] + + // ❌ WRONG: Without initial value, first element becomes accumulator + // This would try to add 500 to an object, resulting in string concatenation + const wrongTotal = products.reduce((acc, p) => acc + p.price) + expect(typeof wrongTotal).toBe('string') // "[object Object]500" + + // ✓ CORRECT: Provide initial value + const correctTotal = products.reduce((acc, p) => acc + p.price, 0) + expect(correctTotal).toBe(1500) + }) + + it('demonstrates forgetting to return accumulator in reduce', () => { + const numbers = [1, 2, 3, 4] + + // ❌ WRONG: No return + const wrong = numbers.reduce((acc, n) => { + acc + n // Missing return! + }, 0) + expect(wrong).toBeUndefined() + + // ✓ CORRECT: Return accumulator + const correct = numbers.reduce((acc, n) => { + return acc + n + }, 0) + expect(correct).toBe(10) + }) + + it('shows filter+map is clearer than complex reduce', () => { + const users = [ + { name: 'Alice', active: true }, + { name: 'Bob', active: false }, + { name: 'Charlie', active: true } + ] + + // Complex reduce approach + const resultReduce = users.reduce((acc, user) => { + if (user.active) { + acc.push(user.name.toUpperCase()) + } + return acc + }, []) + + // Clearer filter + map approach + const resultFilterMap = users + .filter(u => u.active) + .map(u => u.name.toUpperCase()) + + // Both should produce the same result + expect(resultReduce).toEqual(['ALICE', 'CHARLIE']) + expect(resultFilterMap).toEqual(['ALICE', 'CHARLIE']) + }) + }) + + describe('Test Your Knowledge Examples', () => { + it('Q6: filter evens, triple, sum equals 18', () => { + const result = [1, 2, 3, 4, 5] + .filter(n => n % 2 === 0) + .map(n => n * 3) + .reduce((sum, n) => sum + n, 0) + + // filter: [2, 4] + // map: [6, 12] + // reduce: 6 + 12 = 18 + expect(result).toBe(18) + }) + }) + + describe('ES2023+ Array Methods', () => { + it('reduceRight() reduces from right to left', () => { + const letters = ['a', 'b', 'c'] + const result = letters.reduceRight((acc, s) => acc + s, '') + + expect(result).toBe('cba') + }) + + it('toSorted() returns sorted copy without mutating original', () => { + const nums = [3, 1, 2] + const sorted = nums.toSorted() + + expect(sorted).toEqual([1, 2, 3]) + expect(nums).toEqual([3, 1, 2]) // Original unchanged + }) + + it('toReversed() returns reversed copy without mutating original', () => { + const nums = [1, 2, 3] + const reversed = nums.toReversed() + + expect(reversed).toEqual([3, 2, 1]) + expect(nums).toEqual([1, 2, 3]) // Original unchanged + }) + + it('toSpliced() returns modified copy without mutating original', () => { + const nums = [1, 2, 3, 4, 5] + const spliced = nums.toSpliced(1, 2, 'a', 'b') + + expect(spliced).toEqual([1, 'a', 'b', 4, 5]) + expect(nums).toEqual([1, 2, 3, 4, 5]) // Original unchanged + }) + + it('Object.groupBy() groups elements by key (ES2024, Node 21+)', () => { + // Skip test if Object.groupBy is not available (requires Node 21+) + if (typeof Object.groupBy !== 'function') { + console.log('Skipping: Object.groupBy not available in this Node version') + return + } + + const people = [ + { name: 'Alice', department: 'Engineering' }, + { name: 'Bob', department: 'Marketing' }, + { name: 'Charlie', department: 'Engineering' } + ] + + const byDepartment = Object.groupBy(people, person => person.department) + + expect(byDepartment.Engineering).toEqual([ + { name: 'Alice', department: 'Engineering' }, + { name: 'Charlie', department: 'Engineering' } + ]) + expect(byDepartment.Marketing).toEqual([ + { name: 'Bob', department: 'Marketing' } + ]) + }) + }) + + describe('Async Callbacks', () => { + it('map with async returns array of Promises', async () => { + const ids = [1, 2, 3] + + // Simulate async operation + const asyncDouble = async (n) => n * 2 + + // Without Promise.all, you get Promises + const promiseArray = ids.map(id => asyncDouble(id)) + + expect(promiseArray[0]).toBeInstanceOf(Promise) + + // With Promise.all, you get resolved values + const results = await Promise.all(promiseArray) + expect(results).toEqual([2, 4, 6]) + }) + + it('async filter workaround using map then filter', async () => { + const numbers = [1, 2, 3, 4, 5] + + // Simulate async predicate + const asyncIsEven = async (n) => n % 2 === 0 + + // Step 1: Get boolean results for each element + const checks = await Promise.all(numbers.map(n => asyncIsEven(n))) + + // Step 2: Filter using the boolean results + const evens = numbers.filter((_, index) => checks[index]) + + expect(evens).toEqual([2, 4]) + }) + }) +}) diff --git a/tests/functional-programming/pure-functions/pure-functions.test.js b/tests/functional-programming/pure-functions/pure-functions.test.js new file mode 100644 index 00000000..d73b88cc --- /dev/null +++ b/tests/functional-programming/pure-functions/pure-functions.test.js @@ -0,0 +1,809 @@ +import { describe, it, expect } from 'vitest' + +describe('Pure Functions', () => { + describe('Rule 1: Same Input → Same Output', () => { + it('should always return the same result for the same inputs', () => { + // Pure function: deterministic + function add(a, b) { + return a + b + } + + expect(add(2, 3)).toBe(5) + expect(add(2, 3)).toBe(5) + expect(add(2, 3)).toBe(5) + // Always 5, no matter how many times we call it + }) + + it('should demonstrate Math.max as a pure function', () => { + // Math.max is pure: same inputs always give same output + expect(Math.max(2, 8, 5)).toBe(8) + expect(Math.max(2, 8, 5)).toBe(8) + expect(Math.max(-1, -5, -2)).toBe(-1) + }) + + it('should show how external state breaks purity', () => { + // Impure: depends on external state + let taxRate = 0.08 + + function calculateTotalImpure(price) { + return price + price * taxRate + } + + expect(calculateTotalImpure(100)).toBe(108) + + // Changing external state changes the result + taxRate = 0.10 + expect(calculateTotalImpure(100)).toBe(110) // Different! + + // Pure version: all dependencies are parameters + function calculateTotalPure(price, rate) { + return price + price * rate + } + + expect(calculateTotalPure(100, 0.08)).toBe(108) + expect(calculateTotalPure(100, 0.08)).toBe(108) // Always the same + expect(calculateTotalPure(100, 0.10)).toBe(110) // Different input = different output (that's fine) + }) + + it('should demonstrate that Math.random makes functions impure', () => { + // ❌ IMPURE: Output depends on randomness + function randomDouble(x) { + return x * Math.random() + } + + // Same input but (almost certainly) different outputs + const results = new Set() + for (let i = 0; i < 10; i++) { + results.add(randomDouble(5)) + } + + // With random, we get multiple different results for the same input + expect(results.size).toBeGreaterThan(1) + }) + + it('should demonstrate that Date makes functions impure', () => { + // ❌ IMPURE: Output depends on when you call it + function getGreeting(name) { + const hour = new Date().getHours() + if (hour < 12) return `Good morning, ${name}` + return `Good afternoon, ${name}` + } + + // The function works, but its output depends on external state (time) + const result = getGreeting('Alice') + expect(result).toMatch(/Good (morning|afternoon), Alice/) + + // To make it pure, pass the hour as a parameter + function getGreetingPure(name, hour) { + if (hour < 12) return `Good morning, ${name}` + return `Good afternoon, ${name}` + } + + // Now it's deterministic + expect(getGreetingPure('Alice', 9)).toBe('Good morning, Alice') + expect(getGreetingPure('Alice', 9)).toBe('Good morning, Alice') // Always same + expect(getGreetingPure('Alice', 14)).toBe('Good afternoon, Alice') + }) + }) + + describe('Rule 2: No Side Effects', () => { + it('should demonstrate addToTotal impure pattern from docs', () => { + // ❌ IMPURE: Breaks rule 2 (has a side effect) + let total = 0 + + function addToTotal(x) { + total += x // Modifies external variable! + return total + } + + expect(addToTotal(5)).toBe(5) + expect(addToTotal(5)).toBe(10) // Different result because total changed + expect(addToTotal(5)).toBe(15) // Keeps changing! + + // The function modifies external state, making it impure + expect(total).toBe(15) + }) + + it('should demonstrate mutation as a side effect', () => { + // Impure: mutates the input + function addItemImpure(cart, item) { + cart.push(item) + return cart + } + + const myCart = ['apple', 'banana'] + const result = addItemImpure(myCart, 'orange') + + expect(myCart).toEqual(['apple', 'banana', 'orange']) // Original mutated! + expect(result).toBe(myCart) // Same reference + }) + + it('should show pure alternative that returns new array', () => { + // Pure: returns new array, original unchanged + function addItemPure(cart, item) { + return [...cart, item] + } + + const myCart = ['apple', 'banana'] + const newCart = addItemPure(myCart, 'orange') + + expect(myCart).toEqual(['apple', 'banana']) // Original unchanged! + expect(newCart).toEqual(['apple', 'banana', 'orange']) + expect(myCart).not.toBe(newCart) // Different references + }) + + it('should demonstrate external variable modification as a side effect', () => { + let counter = 0 + + // Impure: modifies external variable + function incrementImpure() { + counter++ + return counter + } + + expect(incrementImpure()).toBe(1) + expect(incrementImpure()).toBe(2) // Different result for same (no) input! + expect(incrementImpure()).toBe(3) + + // Pure alternative + function incrementPure(value) { + return value + 1 + } + + expect(incrementPure(0)).toBe(1) + expect(incrementPure(0)).toBe(1) // Always the same + expect(incrementPure(5)).toBe(6) + }) + + it('should demonstrate processUser impure vs pure from docs', () => { + // ❌ IMPURE: Multiple side effects + let userCount = 0 + const loginTime = new Date('2025-01-01T10:00:00') + + function processUserImpure(user) { + user.lastLogin = loginTime // Side effect: mutates input + userCount++ // Side effect: modifies external variable + return user + } + + const user1 = { name: 'Alice' } + const result1 = processUserImpure(user1) + + expect(user1.lastLogin).toEqual(loginTime) // Original mutated! + expect(userCount).toBe(1) // External state changed! + expect(result1).toBe(user1) // Same reference + + // ✓ PURE: Returns new data, no side effects + function processUserPure(user, loginTime) { + return { + ...user, + lastLogin: loginTime + } + } + + const user2 = { name: 'Bob' } + const result2 = processUserPure(user2, loginTime) + + expect(user2.lastLogin).toBe(undefined) // Original unchanged! + expect(result2.lastLogin).toEqual(loginTime) + expect(result2).not.toBe(user2) // Different reference + expect(result2.name).toBe('Bob') + }) + }) + + describe('Identifying Pure vs Impure Functions', () => { + it('should identify pure mathematical functions', () => { + function double(x) { + return x * 2 + } + + function square(x) { + return x * x + } + + function hypotenuse(a, b) { + return Math.sqrt(a * a + b * b) + } + + // All pure: same inputs always give same outputs + expect(double(5)).toBe(10) + expect(square(4)).toBe(16) + expect(hypotenuse(3, 4)).toBe(5) + }) + + it('should identify pure string functions', () => { + function formatName(name) { + return name.trim().toLowerCase() + } + + function greet(name, greeting) { + return `${greeting}, ${name}!` + } + + expect(formatName(' ALICE ')).toBe('alice') + expect(formatName(' ALICE ')).toBe('alice') // Same result + expect(greet('Bob', 'Hello')).toBe('Hello, Bob!') + }) + + it('should identify pure validation functions', () => { + function isValidEmail(email) { + return email.includes('@') && email.includes('.') + } + + function isPositive(num) { + return num > 0 + } + + expect(isValidEmail('test@example.com')).toBe(true) + expect(isValidEmail('invalid')).toBe(false) + expect(isPositive(5)).toBe(true) + expect(isPositive(-3)).toBe(false) + }) + }) + + describe('Immutable Object Patterns', () => { + it('should update object properties without mutation', () => { + const user = { name: 'Alice', age: 25 } + + // Pure: returns new object + function updateAge(user, newAge) { + return { ...user, age: newAge } + } + + const updatedUser = updateAge(user, 26) + + expect(user.age).toBe(25) // Original unchanged + expect(updatedUser.age).toBe(26) + expect(user).not.toBe(updatedUser) + }) + + it('should add properties without mutation', () => { + const product = { name: 'Widget', price: 10 } + + function addDiscount(product, discount) { + return { ...product, discount } + } + + const discountedProduct = addDiscount(product, 0.1) + + expect(product.discount).toBe(undefined) // Original unchanged + expect(discountedProduct.discount).toBe(0.1) + }) + + it('should remove properties without mutation', () => { + const user = { name: 'Alice', age: 25, password: 'secret' } + + function removePassword(user) { + const { password, ...rest } = user + return rest + } + + const safeUser = removePassword(user) + + expect(user.password).toBe('secret') // Original unchanged + expect(safeUser.password).toBe(undefined) + expect(safeUser).toEqual({ name: 'Alice', age: 25 }) + }) + }) + + describe('Immutable Array Patterns', () => { + it('should add items without mutation', () => { + const todos = ['Learn JS', 'Build app'] + + // Pure: returns new array + function addTodo(todos, newTodo) { + return [...todos, newTodo] + } + + const newTodos = addTodo(todos, 'Deploy') + + expect(todos).toEqual(['Learn JS', 'Build app']) // Original unchanged + expect(newTodos).toEqual(['Learn JS', 'Build app', 'Deploy']) + }) + + it('should remove items without mutation', () => { + const numbers = [1, 2, 3, 4, 5] + + // Pure: filter creates new array + function removeItem(arr, index) { + return arr.filter((_, i) => i !== index) + } + + const result = removeItem(numbers, 2) // Remove item at index 2 + + expect(numbers).toEqual([1, 2, 3, 4, 5]) // Original unchanged + expect(result).toEqual([1, 2, 4, 5]) + }) + + it('should update items without mutation', () => { + const todos = [ + { id: 1, text: 'Learn JS', done: false }, + { id: 2, text: 'Build app', done: false } + ] + + function completeTodo(todos, id) { + return todos.map((todo) => (todo.id === id ? { ...todo, done: true } : todo)) + } + + const updated = completeTodo(todos, 1) + + expect(todos[0].done).toBe(false) // Original unchanged + expect(updated[0].done).toBe(true) + expect(updated[1].done).toBe(false) + }) + + it('should sort without mutation using spread', () => { + const numbers = [3, 1, 4, 1, 5, 9, 2, 6] + + // Impure: sort mutates the original + function sortImpure(arr) { + return arr.sort((a, b) => a - b) + } + + // Pure: copy first + function sortPure(arr) { + return [...arr].sort((a, b) => a - b) + } + + const sorted = sortPure(numbers) + + expect(numbers).toEqual([3, 1, 4, 1, 5, 9, 2, 6]) // Original unchanged + expect(sorted).toEqual([1, 1, 2, 3, 4, 5, 6, 9]) + }) + + it('should use toSorted for non-mutating sort (ES2023)', () => { + const numbers = [3, 1, 4, 1, 5] + + const sorted = numbers.toSorted((a, b) => a - b) + + expect(numbers).toEqual([3, 1, 4, 1, 5]) // Original unchanged + expect(sorted).toEqual([1, 1, 3, 4, 5]) + }) + + it('should use toReversed for non-mutating reverse (ES2023)', () => { + const letters = ['a', 'b', 'c', 'd'] + + const reversed = letters.toReversed() + + expect(letters).toEqual(['a', 'b', 'c', 'd']) // Original unchanged + expect(reversed).toEqual(['d', 'c', 'b', 'a']) + }) + }) + + describe('Deep Copy for Nested Objects', () => { + it('should demonstrate shallow copy problem with nested objects', () => { + const user = { + name: 'Alice', + address: { city: 'NYC', zip: '10001' } + } + + // Shallow copy - nested object is shared! + const shallowCopy = { ...user } + + shallowCopy.address.city = 'LA' + + expect(user.address.city).toBe('LA') // Original changed! + }) + + it('should use structuredClone for deep copy', () => { + const user = { + name: 'Alice', + address: { city: 'NYC', zip: '10001' } + } + + const deepCopy = structuredClone(user) + + deepCopy.address.city = 'LA' + + expect(user.address.city).toBe('NYC') // Original unchanged! + expect(deepCopy.address.city).toBe('LA') + }) + + it('should safely update nested properties in pure function', () => { + const user = { + name: 'Alice', + address: { city: 'NYC', zip: '10001' } + } + + // Pure function using structuredClone + function updateCity(user, newCity) { + const copy = structuredClone(user) + copy.address.city = newCity + return copy + } + + // Alternative: spread at each level + function updateCitySpread(user, newCity) { + return { + ...user, + address: { + ...user.address, + city: newCity + } + } + } + + const updated1 = updateCity(user, 'LA') + const updated2 = updateCitySpread(user, 'Boston') + + expect(user.address.city).toBe('NYC') // Original unchanged + expect(updated1.address.city).toBe('LA') + expect(updated2.address.city).toBe('Boston') + }) + }) + + describe('Common Mistakes', () => { + it('should avoid mutating function parameters', () => { + // Bad: mutates the parameter + function processUserBad(user) { + user.processed = true + user.name = user.name.toUpperCase() + return user + } + + // Good: returns new object + function processUserGood(user) { + return { + ...user, + processed: true, + name: user.name.toUpperCase() + } + } + + const user = { name: 'alice', age: 25 } + + const result = processUserGood(user) + + expect(user.processed).toBe(undefined) // Original unchanged + expect(user.name).toBe('alice') + expect(result.processed).toBe(true) + expect(result.name).toBe('ALICE') + }) + + it('should avoid relying on external mutable state', () => { + // Bad: relies on external config + const config = { multiplier: 2 } + + function calculateBad(value) { + return value * config.multiplier + } + + // Good: config passed as parameter + function calculateGood(value, multiplier) { + return value * multiplier + } + + expect(calculateGood(5, 2)).toBe(10) + expect(calculateGood(5, 2)).toBe(10) // Always predictable + }) + + it('should be careful with array methods that mutate', () => { + const numbers = [3, 1, 2] + + // These methods MUTATE the original array: + // sort(), reverse(), splice(), push(), pop(), shift(), unshift(), fill() + + // Safe alternatives: + const sorted = [...numbers].sort((a, b) => a - b) // Copy first + const reversed = [...numbers].reverse() // Copy first + const withNew = [...numbers, 4] // Spread instead of push + + expect(numbers).toEqual([3, 1, 2]) // Original unchanged + expect(sorted).toEqual([1, 2, 3]) + expect(reversed).toEqual([2, 1, 3]) + expect(withNew).toEqual([3, 1, 2, 4]) + }) + }) + + describe('Practical Pure Function Examples', () => { + it('should calculate shopping cart total purely', () => { + function calculateTotal(items, taxRate) { + const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0) + const tax = subtotal * taxRate + return { + subtotal, + tax, + total: subtotal + tax + } + } + + const items = [ + { name: 'Widget', price: 10, quantity: 2 }, + { name: 'Gadget', price: 25, quantity: 1 } + ] + + const result = calculateTotal(items, 0.08) + + expect(result.subtotal).toBe(45) + expect(result.tax).toBeCloseTo(3.6) + expect(result.total).toBeCloseTo(48.6) + + // Original items unchanged + expect(items[0].name).toBe('Widget') + }) + + it('should filter and transform data purely', () => { + function getActiveUserNames(users) { + return users.filter((user) => user.active).map((user) => user.name.toLowerCase()) + } + + const users = [ + { name: 'ALICE', active: true }, + { name: 'BOB', active: false }, + { name: 'CHARLIE', active: true } + ] + + const result = getActiveUserNames(users) + + expect(result).toEqual(['alice', 'charlie']) + expect(users[0].name).toBe('ALICE') // Original unchanged + }) + + it('should compose pure functions', () => { + const trim = (str) => str.trim() + const toLowerCase = (str) => str.toLowerCase() + const removeSpaces = (str) => str.replace(/\s+/g, '-') + + function slugify(title) { + return removeSpaces(toLowerCase(trim(title))) + } + + expect(slugify(' Hello World ')).toBe('hello-world') + expect(slugify(' JavaScript Is Fun ')).toBe('javascript-is-fun') + }) + + it('should validate data purely', () => { + function validateUser(user) { + const errors = [] + + if (!user.name || user.name.length < 2) { + errors.push('Name must be at least 2 characters') + } + + if (!user.email || !user.email.includes('@')) { + errors.push('Valid email is required') + } + + if (!user.age || user.age < 0) { + errors.push('Age must be a positive number') + } + + return { + isValid: errors.length === 0, + errors + } + } + + const validUser = { name: 'Alice', email: 'alice@example.com', age: 25 } + const invalidUser = { name: 'A', email: 'invalid', age: -5 } + + expect(validateUser(validUser).isValid).toBe(true) + expect(validateUser(validUser).errors).toEqual([]) + + expect(validateUser(invalidUser).isValid).toBe(false) + expect(validateUser(invalidUser).errors).toHaveLength(3) + }) + }) + + describe('Benefits of Pure Functions', () => { + it('should be easy to test (no setup needed)', () => { + // Pure functions are trivial to test + function add(a, b) { + return a + b + } + + // No mocking, no setup, no cleanup + expect(add(1, 2)).toBe(3) + expect(add(-1, 1)).toBe(0) + expect(add(0.1, 0.2)).toBeCloseTo(0.3) + }) + + it('should be safe to memoize', () => { + let callCount = 0 + + // Pure function - safe to cache + function expensiveCalculation(n) { + callCount++ + let result = 0 + for (let i = 0; i < n; i++) { + result += i + } + return result + } + + // Simple memoization + function memoize(fn) { + const cache = new Map() + return function (arg) { + if (cache.has(arg)) { + return cache.get(arg) + } + const result = fn(arg) + cache.set(arg, result) + return result + } + } + + const memoizedCalc = memoize(expensiveCalculation) + + // First call computes + expect(memoizedCalc(1000)).toBe(499500) + expect(callCount).toBe(1) + + // Second call returns cached result + expect(memoizedCalc(1000)).toBe(499500) + expect(callCount).toBe(1) // Not called again! + + // Different input computes again + expect(memoizedCalc(500)).toBe(124750) + expect(callCount).toBe(2) + }) + + it('should demonstrate fibonacci as a pure function safe for memoization', () => { + // Expensive calculation - safe to cache because it's pure + function fibonacci(n) { + if (n <= 1) return n + return fibonacci(n - 1) + fibonacci(n - 2) + } + + // Pure: same input always gives same output + expect(fibonacci(0)).toBe(0) + expect(fibonacci(1)).toBe(1) + expect(fibonacci(2)).toBe(1) + expect(fibonacci(3)).toBe(2) + expect(fibonacci(4)).toBe(3) + expect(fibonacci(5)).toBe(5) + expect(fibonacci(10)).toBe(55) + + // Call multiple times - always same result + expect(fibonacci(10)).toBe(55) + expect(fibonacci(10)).toBe(55) + }) + }) + + describe('Examples from Q&A Section', () => { + it('should demonstrate multiply as a pure function', () => { + // Pure: follows both rules + function multiply(a, b) { + return a * b + } + + expect(multiply(3, 4)).toBe(12) + expect(multiply(3, 4)).toBe(12) // Always the same + expect(multiply(-2, 5)).toBe(-10) + expect(multiply(0, 100)).toBe(0) + }) + + it('should demonstrate greet impure vs pure', () => { + // ❌ IMPURE: Uses new Date() - output varies with time + function greetImpure(name) { + return `Hello, ${name}! The time is ${new Date().toLocaleTimeString()}` + } + + // The impure version includes time, making results unpredictable + const result1 = greetImpure('Alice') + expect(result1).toContain('Hello, Alice!') + expect(result1).toContain('The time is') + + // ✓ PURE: Pass time as a parameter + function greetPure(name, time) { + return `Hello, ${name}! The time is ${time}` + } + + expect(greetPure('Alice', '10:00:00 AM')).toBe('Hello, Alice! The time is 10:00:00 AM') + expect(greetPure('Alice', '10:00:00 AM')).toBe('Hello, Alice! The time is 10:00:00 AM') // Always same + expect(greetPure('Bob', '3:00:00 PM')).toBe('Hello, Bob! The time is 3:00:00 PM') + }) + + it('should demonstrate calculateTax as a pure function', () => { + // If calculateTax(100, 0.08) returns the wrong value, + // the bug MUST be inside calculateTax. + // No need to check what other code ran before it. + function calculateTax(amount, rate) { + return amount * rate + } + + expect(calculateTax(100, 0.08)).toBe(8) + expect(calculateTax(100, 0.08)).toBe(8) // Always the same + expect(calculateTax(250, 0.1)).toBe(25) + expect(calculateTax(0, 0.08)).toBe(0) + }) + + it('should demonstrate formatPrice as a pure function', () => { + // You can understand this function completely by reading it + function formatPrice(cents, currency = 'USD') { + const dollars = cents / 100 + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency + }).format(dollars) + } + + expect(formatPrice(1999)).toBe('$19.99') + expect(formatPrice(1999)).toBe('$19.99') // Always the same + expect(formatPrice(500)).toBe('$5.00') + expect(formatPrice(9999, 'USD')).toBe('$99.99') + expect(formatPrice(1000, 'EUR')).toBe('€10.00') + }) + + it('should demonstrate addToCart fix from Q&A', () => { + // ❌ WRONG: This function mutates its input + function addToCartBad(cart, item) { + cart.push(item) + return cart + } + + const cart1 = ['apple'] + const result1 = addToCartBad(cart1, 'banana') + expect(cart1).toEqual(['apple', 'banana']) // Original mutated! + expect(result1).toBe(cart1) // Same reference + + // ✓ CORRECT: Fix it by returning a new array + function addToCartGood(cart, item) { + return [...cart, item] + } + + const cart2 = ['apple'] + const result2 = addToCartGood(cart2, 'banana') + expect(cart2).toEqual(['apple']) // Original unchanged! + expect(result2).toEqual(['apple', 'banana']) + expect(result2).not.toBe(cart2) // Different reference + }) + + it('should demonstrate updateCity with structuredClone from Q&A', () => { + const user = { + name: 'Alice', + address: { city: 'NYC', zip: '10001' } + } + + // Option 1: structuredClone (simplest) + function updateCityClone(user, newCity) { + const copy = structuredClone(user) + copy.address.city = newCity + return copy + } + + const updated1 = updateCityClone(user, 'LA') + expect(user.address.city).toBe('NYC') // Original unchanged + expect(updated1.address.city).toBe('LA') + + // Option 2: Spread at each level + function updateCitySpread(user, newCity) { + return { + ...user, + address: { + ...user.address, + city: newCity + } + } + } + + const updated2 = updateCitySpread(user, 'Boston') + expect(user.address.city).toBe('NYC') // Original still unchanged + expect(updated2.address.city).toBe('Boston') + }) + }) + + describe('Examples from Accordion Sections', () => { + it('should demonstrate testing pure functions is trivial', () => { + // Testing a pure function - simple and straightforward + function add(a, b) { + return a + b + } + + function formatName(name) { + return name.trim().toLowerCase() + } + + function isValidEmail(email) { + return email.includes('@') && email.includes('.') + } + + // No mocking, no setup - just input and expected output + expect(add(2, 3)).toBe(5) + expect(formatName(' ALICE ')).toBe('alice') + expect(isValidEmail('test@example.com')).toBe(true) + expect(isValidEmail('invalid')).toBe(false) + }) + }) +}) diff --git a/tests/functional-programming/recursion/recursion.test.js b/tests/functional-programming/recursion/recursion.test.js new file mode 100644 index 00000000..6f3d56ca --- /dev/null +++ b/tests/functional-programming/recursion/recursion.test.js @@ -0,0 +1,933 @@ +import { describe, it, expect } from 'vitest' +import { JSDOM } from 'jsdom' + +describe('Recursion', () => { + describe('Base Case Handling', () => { + it('should return immediately when base case is met', () => { + function countdown(n) { + if (n <= 0) return 'done' + return countdown(n - 1) + } + + expect(countdown(0)).toBe('done') + expect(countdown(-1)).toBe('done') + }) + + it('should demonstrate countdown pattern from MDX opening example', () => { + // Exact implementation from MDX lines 9-17 (modified to collect output) + // Original uses console.log, we collect to array for testing + function countdown(n, output = []) { + if (n === 0) { + output.push('Done!') + return output + } + output.push(n) + return countdown(n - 1, output) + } + + // MDX example: countdown(3) outputs 3, 2, 1, Done! + expect(countdown(3)).toEqual([3, 2, 1, 'Done!']) + expect(countdown(1)).toEqual([1, 'Done!']) + expect(countdown(0)).toEqual(['Done!']) + }) + + it('should throw RangeError for infinite recursion (missing base case)', () => { + function infiniteRecursion(n) { + // No base case - will crash + return infiniteRecursion(n - 1) + } + + expect(() => infiniteRecursion(5)).toThrow(RangeError) + }) + + it('should handle base case that returns a value', () => { + function sumTo(n) { + if (n === 1) return 1 + return n + sumTo(n - 1) + } + + expect(sumTo(1)).toBe(1) + }) + }) + + describe('Classic Algorithms', () => { + describe('Factorial', () => { + function factorial(n) { + if (n <= 1) return 1 + return n * factorial(n - 1) + } + + it('should calculate factorial correctly', () => { + expect(factorial(5)).toBe(120) + expect(factorial(4)).toBe(24) + expect(factorial(3)).toBe(6) + }) + + it('should handle edge cases (0! = 1, 1! = 1)', () => { + expect(factorial(0)).toBe(1) + expect(factorial(1)).toBe(1) + }) + + it('should handle larger numbers', () => { + expect(factorial(10)).toBe(3628800) + }) + }) + + describe('Fibonacci', () => { + // Memoized version for efficiency + function fibonacci(n, memo = {}) { + if (n in memo) return memo[n] + if (n <= 1) return n + memo[n] = fibonacci(n - 1, memo) + fibonacci(n - 2, memo) + return memo[n] + } + + it('should return correct Fibonacci numbers', () => { + expect(fibonacci(6)).toBe(8) + expect(fibonacci(7)).toBe(13) + expect(fibonacci(10)).toBe(55) + }) + + it('should handle base cases (fib(0) = 0, fib(1) = 1)', () => { + expect(fibonacci(0)).toBe(0) + expect(fibonacci(1)).toBe(1) + }) + + it('should handle larger numbers efficiently with memoization', () => { + expect(fibonacci(50)).toBe(12586269025) + }) + + it('should follow the Fibonacci sequence pattern', () => { + // Each number is sum of two preceding ones + const sequence = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] + sequence.forEach((expected, index) => { + expect(fibonacci(index)).toBe(expected) + }) + }) + }) + + describe('Sum to N', () => { + function sumTo(n) { + if (n <= 1) return n + return n + sumTo(n - 1) + } + + it('should sum numbers from 1 to n', () => { + expect(sumTo(5)).toBe(15) // 1+2+3+4+5 + expect(sumTo(10)).toBe(55) + expect(sumTo(100)).toBe(5050) + }) + + it('should handle base cases', () => { + expect(sumTo(1)).toBe(1) + expect(sumTo(0)).toBe(0) + }) + }) + + describe('Power Function', () => { + function power(x, n) { + if (n === 0) return 1 + return x * power(x, n - 1) + } + + it('should calculate x^n correctly', () => { + expect(power(2, 3)).toBe(8) + expect(power(2, 10)).toBe(1024) + expect(power(3, 4)).toBe(81) + }) + + it('should handle power of 0', () => { + expect(power(5, 0)).toBe(1) + expect(power(100, 0)).toBe(1) + }) + + it('should handle power of 1', () => { + expect(power(7, 1)).toBe(7) + }) + }) + + describe('Power Function (Optimized O(log n))', () => { + function powerFast(x, n) { + if (n === 0) return 1 + + if (n % 2 === 0) { + // Even exponent: x^n = (x^(n/2))^2 + const half = powerFast(x, n / 2) + return half * half + } else { + // Odd exponent: x^n = x * x^(n-1) + return x * powerFast(x, n - 1) + } + } + + it('should calculate x^n correctly with O(log n) complexity', () => { + expect(powerFast(2, 10)).toBe(1024) + expect(powerFast(3, 4)).toBe(81) + expect(powerFast(2, 3)).toBe(8) + }) + + it('should handle even exponents efficiently', () => { + expect(powerFast(2, 8)).toBe(256) + expect(powerFast(2, 16)).toBe(65536) + expect(powerFast(5, 4)).toBe(625) + }) + + it('should handle odd exponents', () => { + expect(powerFast(3, 5)).toBe(243) + expect(powerFast(2, 7)).toBe(128) + }) + + it('should handle edge cases', () => { + expect(powerFast(5, 0)).toBe(1) + expect(powerFast(7, 1)).toBe(7) + expect(powerFast(100, 0)).toBe(1) + }) + + it('should produce same results as naive power function', () => { + function powerNaive(x, n) { + if (n === 0) return 1 + return x * powerNaive(x, n - 1) + } + + // Test that both implementations produce identical results + for (let x = 1; x <= 5; x++) { + for (let n = 0; n <= 10; n++) { + expect(powerFast(x, n)).toBe(powerNaive(x, n)) + } + } + }) + }) + + describe('String Reversal', () => { + function reverse(str) { + if (str.length <= 1) return str + return str[str.length - 1] + reverse(str.slice(0, -1)) + } + + it('should reverse a string', () => { + expect(reverse('hello')).toBe('olleh') + expect(reverse('world')).toBe('dlrow') + expect(reverse('recursion')).toBe('noisrucer') + }) + + it('should handle edge cases', () => { + expect(reverse('')).toBe('') + expect(reverse('a')).toBe('a') + }) + }) + }) + + describe('Practical Patterns', () => { + describe('Array Flattening', () => { + function flatten(arr) { + let result = [] + for (const item of arr) { + if (Array.isArray(item)) { + result = result.concat(flatten(item)) + } else { + result.push(item) + } + } + return result + } + + it('should flatten nested arrays', () => { + expect(flatten([1, [2, [3, 4]], 5])).toEqual([1, 2, 3, 4, 5]) + expect(flatten([1, [2, [3, [4, [5]]]]])).toEqual([1, 2, 3, 4, 5]) + }) + + it('should handle already flat arrays', () => { + expect(flatten([1, 2, 3])).toEqual([1, 2, 3]) + }) + + it('should handle empty arrays', () => { + expect(flatten([])).toEqual([]) + expect(flatten([[], []])).toEqual([]) + }) + }) + + describe('Nested Object Traversal', () => { + function findAllValues(obj, key) { + let results = [] + for (const k in obj) { + if (k === key) { + results.push(obj[k]) + } else if (typeof obj[k] === 'object' && obj[k] !== null) { + results = results.concat(findAllValues(obj[k], key)) + } + } + return results + } + + it('should find all values for a given key in nested objects', () => { + const data = { + name: 'root', + children: { + a: { name: 'a', value: 1 }, + b: { name: 'b', value: 2 } + } + } + + expect(findAllValues(data, 'name')).toEqual(['root', 'a', 'b']) + expect(findAllValues(data, 'value')).toEqual([1, 2]) + }) + + it('should return empty array if key not found', () => { + const data = { a: 1, b: 2 } + expect(findAllValues(data, 'notfound')).toEqual([]) + }) + }) + + describe('Finding All Counts (MDX Example)', () => { + // Exact implementation from MDX lines 410-423 + function findAllCounts(obj) { + let total = 0 + + for (const key in obj) { + if (key === 'count') { + total += obj[key] + } else if (typeof obj[key] === 'object' && obj[key] !== null) { + // Recurse into nested objects + total += findAllCounts(obj[key]) + } + } + + return total + } + + it('should find and sum all count values in nested object (MDX example)', () => { + const data = { + name: 'Company', + departments: { + engineering: { + frontend: { count: 5 }, + backend: { count: 8 } + }, + sales: { count: 12 } + } + } + + expect(findAllCounts(data)).toBe(25) // 5 + 8 + 12 + }) + + it('should return 0 for empty object', () => { + expect(findAllCounts({})).toBe(0) + }) + + it('should return 0 when no count keys exist', () => { + const data = { + name: 'Test', + nested: { + value: 10, + deeper: { something: 'else' } + } + } + expect(findAllCounts(data)).toBe(0) + }) + + it('should handle flat object with count', () => { + expect(findAllCounts({ count: 42 })).toBe(42) + }) + + it('should handle deeply nested counts', () => { + const data = { + level1: { + level2: { + level3: { + level4: { + count: 100 + } + } + } + } + } + expect(findAllCounts(data)).toBe(100) + }) + }) + + describe('Linked List Operations', () => { + function sumList(node) { + if (node === null) return 0 + return node.value + sumList(node.next) + } + + function listLength(node) { + if (node === null) return 0 + return 1 + listLength(node.next) + } + + // Modified version of MDX printReverse that collects results instead of console.log + // MDX implementation (lines 505-509): + // function printReverse(node) { + // if (node === null) return + // printReverse(node.next) // First, go to the end + // console.log(node.value) // Then print on the way back + // } + function collectReverse(node, results = []) { + if (node === null) return results + collectReverse(node.next, results) // First, go to the end + results.push(node.value) // Then collect on the way back + return results + } + + const list = { + value: 1, + next: { + value: 2, + next: { + value: 3, + next: null + } + } + } + + it('should sum all values in a linked list', () => { + expect(sumList(list)).toBe(6) + }) + + it('should count nodes in a linked list', () => { + expect(listLength(list)).toBe(3) + }) + + it('should handle empty list (null)', () => { + expect(sumList(null)).toBe(0) + expect(listLength(null)).toBe(0) + }) + + it('should handle single node list', () => { + const single = { value: 5, next: null } + expect(sumList(single)).toBe(5) + expect(listLength(single)).toBe(1) + }) + + it('should collect values in reverse order (printReverse pattern)', () => { + // MDX shows: printReverse(list) outputs 3, 2, 1 + expect(collectReverse(list)).toEqual([3, 2, 1]) + }) + + it('should return empty array for null list (printReverse pattern)', () => { + expect(collectReverse(null)).toEqual([]) + }) + + it('should handle single node for reverse collection', () => { + const single = { value: 42, next: null } + expect(collectReverse(single)).toEqual([42]) + }) + }) + + describe('Tree Node Counting', () => { + function countNodes(node) { + if (node === null) return 0 + return 1 + countNodes(node.left) + countNodes(node.right) + } + + function sumTree(node) { + if (node === null) return 0 + return node.value + sumTree(node.left) + sumTree(node.right) + } + + const tree = { + value: 1, + left: { + value: 2, + left: { value: 4, left: null, right: null }, + right: { value: 5, left: null, right: null } + }, + right: { + value: 3, + left: null, + right: null + } + } + + it('should count all nodes in a tree', () => { + expect(countNodes(tree)).toBe(5) + }) + + it('should sum all values in a tree', () => { + expect(sumTree(tree)).toBe(15) // 1+2+3+4+5 + }) + + it('should handle empty tree', () => { + expect(countNodes(null)).toBe(0) + expect(sumTree(null)).toBe(0) + }) + }) + + describe('File System Traversal (getTotalSize)', () => { + // Exact implementation from MDX lines 539-550 + function getTotalSize(node) { + if (node.type === 'file') { + return node.size + } + + // Folder: sum sizes of all children + let total = 0 + for (const child of node.children) { + total += getTotalSize(child) + } + return total + } + + it('should calculate total size of file system (MDX example)', () => { + // Exact data structure from MDX lines 522-537 + const fileSystem = { + name: 'root', + type: 'folder', + children: [ + { name: 'file1.txt', type: 'file', size: 100 }, + { + name: 'docs', + type: 'folder', + children: [ + { name: 'readme.md', type: 'file', size: 50 }, + { name: 'notes.txt', type: 'file', size: 25 } + ] + } + ] + } + + expect(getTotalSize(fileSystem)).toBe(175) // 100 + 50 + 25 + }) + + it('should return size of single file', () => { + const singleFile = { name: 'test.js', type: 'file', size: 42 } + expect(getTotalSize(singleFile)).toBe(42) + }) + + it('should return 0 for empty folder', () => { + const emptyFolder = { name: 'empty', type: 'folder', children: [] } + expect(getTotalSize(emptyFolder)).toBe(0) + }) + + it('should handle deeply nested folders', () => { + const deepStructure = { + name: 'level0', + type: 'folder', + children: [ + { + name: 'level1', + type: 'folder', + children: [ + { + name: 'level2', + type: 'folder', + children: [{ name: 'deep.txt', type: 'file', size: 999 }] + } + ] + } + ] + } + expect(getTotalSize(deepStructure)).toBe(999) + }) + + it('should sum files across multiple nested folders', () => { + const multiFolder = { + name: 'root', + type: 'folder', + children: [ + { name: 'a.txt', type: 'file', size: 10 }, + { + name: 'sub1', + type: 'folder', + children: [ + { name: 'b.txt', type: 'file', size: 20 }, + { name: 'c.txt', type: 'file', size: 30 } + ] + }, + { + name: 'sub2', + type: 'folder', + children: [{ name: 'd.txt', type: 'file', size: 40 }] + } + ] + } + expect(getTotalSize(multiFolder)).toBe(100) // 10 + 20 + 30 + 40 + }) + }) + + describe('DOM Traversal (walkDOM)', () => { + // Exact implementation from MDX lines 461-470 + function walkDOM(node, callback) { + // Process this node + callback(node) + + // Recurse into child nodes + for (const child of node.children) { + walkDOM(child, callback) + } + } + + it('should collect all tag names in document order (MDX example)', () => { + const dom = new JSDOM(` + <body> + <div> + <p></p> + <span></span> + </div> + <footer></footer> + </body> + `) + + const tagNames = [] + walkDOM(dom.window.document.body, (node) => { + tagNames.push(node.tagName) + }) + + expect(tagNames).toEqual(['BODY', 'DIV', 'P', 'SPAN', 'FOOTER']) + }) + + it('should handle single element with no children', () => { + const dom = new JSDOM(`<body></body>`) + + const tagNames = [] + walkDOM(dom.window.document.body, (node) => { + tagNames.push(node.tagName) + }) + + expect(tagNames).toEqual(['BODY']) + }) + + it('should handle deeply nested structure', () => { + const dom = new JSDOM(` + <body> + <div> + <div> + <div> + <p></p> + </div> + </div> + </div> + </body> + `) + + const tagNames = [] + walkDOM(dom.window.document.body, (node) => { + tagNames.push(node.tagName) + }) + + expect(tagNames).toEqual(['BODY', 'DIV', 'DIV', 'DIV', 'P']) + }) + + it('should process nodes in depth-first order', () => { + const dom = new JSDOM(` + <body> + <nav> + <a></a> + </nav> + <main> + <article> + <h1></h1> + <p></p> + </article> + </main> + </body> + `) + + const tagNames = [] + walkDOM(dom.window.document.body, (node) => { + tagNames.push(node.tagName) + }) + + expect(tagNames).toEqual(['BODY', 'NAV', 'A', 'MAIN', 'ARTICLE', 'H1', 'P']) + }) + + it('should allow custom callbacks', () => { + const dom = new JSDOM(` + <body> + <div id="first"></div> + <div id="second"></div> + </body> + `) + + const ids = [] + walkDOM(dom.window.document.body, (node) => { + if (node.id) { + ids.push(node.id) + } + }) + + expect(ids).toEqual(['first', 'second']) + }) + }) + }) + + describe('Common Mistakes', () => { + it('should demonstrate stack overflow without proper base case', () => { + function badRecursion(n) { + // Base case uses === instead of <=, causing overflow for negative inputs + if (n === 0) return 0 + return badRecursion(n - 2) // Skips 0 when starting with odd number + } + + // Odd number will skip past 0 and cause stack overflow + expect(() => badRecursion(5)).toThrow(RangeError) + }) + + it('should show difference between returning and not returning recursive call', () => { + function withReturn(n) { + if (n === 1) return 1 + return n + withReturn(n - 1) + } + + function withoutReturn(n) { + if (n === 1) return 1 + n + withoutReturn(n - 1) // Missing return! + } + + expect(withReturn(5)).toBe(15) + expect(withoutReturn(5)).toBeUndefined() + }) + }) + + describe('Optimization', () => { + it('should demonstrate memoized fibonacci is much faster than naive', () => { + // Naive implementation (would be very slow for large n) + function fibNaive(n) { + if (n <= 1) return n + return fibNaive(n - 1) + fibNaive(n - 2) + } + + // Memoized implementation + function fibMemo(n, memo = {}) { + if (n in memo) return memo[n] + if (n <= 1) return n + memo[n] = fibMemo(n - 1, memo) + fibMemo(n - 2, memo) + return memo[n] + } + + // Both should return the same result + expect(fibNaive(10)).toBe(55) + expect(fibMemo(10)).toBe(55) + + // But memoized can handle much larger numbers + expect(fibMemo(50)).toBe(12586269025) + // fibNaive(50) would take minutes or crash + }) + + it('should demonstrate tail recursive vs non-tail recursive', () => { + // Non-tail recursive: multiplication happens AFTER recursive call returns + function factorialNonTail(n) { + if (n <= 1) return 1 + return n * factorialNonTail(n - 1) + } + + // Tail recursive: recursive call is the LAST operation + function factorialTail(n, acc = 1) { + if (n <= 1) return acc + return factorialTail(n - 1, acc * n) + } + + // Both produce the same result + expect(factorialNonTail(5)).toBe(120) + expect(factorialTail(5)).toBe(120) + expect(factorialNonTail(10)).toBe(3628800) + expect(factorialTail(10)).toBe(3628800) + }) + }) + + describe('Edge Cases', () => { + it('should handle recursive function with multiple base cases', () => { + function fibonacci(n) { + if (n === 0) return 0 // First base case + if (n === 1) return 1 // Second base case + return fibonacci(n - 1) + fibonacci(n - 2) + } + + expect(fibonacci(0)).toBe(0) + expect(fibonacci(1)).toBe(1) + expect(fibonacci(2)).toBe(1) + }) + + it('should handle recursion with multiple recursive calls', () => { + function sumTree(node) { + if (node === null) return 0 + // Two recursive calls + return node.value + sumTree(node.left) + sumTree(node.right) + } + + const tree = { + value: 10, + left: { value: 5, left: null, right: null }, + right: { value: 15, left: null, right: null } + } + + expect(sumTree(tree)).toBe(30) + }) + + it('should handle mutual recursion', () => { + function isEven(n) { + if (n === 0) return true + return isOdd(n - 1) + } + + function isOdd(n) { + if (n === 0) return false + return isEven(n - 1) + } + + expect(isEven(4)).toBe(true) + expect(isEven(5)).toBe(false) + expect(isOdd(3)).toBe(true) + expect(isOdd(4)).toBe(false) + }) + }) + + describe('Recursion vs Iteration', () => { + describe('Factorial (Iterative vs Recursive)', () => { + // Recursive version (from Classic Algorithms) + function factorialRecursive(n) { + if (n <= 1) return 1 + return n * factorialRecursive(n - 1) + } + + // Exact iterative implementation from MDX lines 585-592 + function factorialIterative(n) { + let result = 1 + for (let i = 2; i <= n; i++) { + result *= i + } + return result + } + + it('should produce same results as recursive version', () => { + expect(factorialIterative(5)).toBe(factorialRecursive(5)) + expect(factorialIterative(10)).toBe(factorialRecursive(10)) + }) + + it('should calculate factorial correctly', () => { + expect(factorialIterative(5)).toBe(120) + expect(factorialIterative(10)).toBe(3628800) + }) + + it('should handle edge cases (0! = 1, 1! = 1)', () => { + expect(factorialIterative(0)).toBe(1) + expect(factorialIterative(1)).toBe(1) + }) + + it('should handle larger numbers without stack overflow', () => { + // Iterative can handle larger numbers without stack concerns + expect(factorialIterative(20)).toBe(2432902008176640000) + }) + }) + + describe('Sum Tree (Iterative with Explicit Stack)', () => { + // Recursive version + function sumTreeRecursive(node) { + if (node === null) return 0 + return node.value + sumTreeRecursive(node.left) + sumTreeRecursive(node.right) + } + + // Exact iterative implementation from MDX lines 814-829 + function sumTreeIterative(root) { + if (root === null) return 0 + + let sum = 0 + const stack = [root] + + while (stack.length > 0) { + const node = stack.pop() + sum += node.value + + if (node.right) stack.push(node.right) + if (node.left) stack.push(node.left) + } + + return sum + } + + const tree = { + value: 1, + left: { + value: 2, + left: { value: 4, left: null, right: null }, + right: { value: 5, left: null, right: null } + }, + right: { + value: 3, + left: null, + right: null + } + } + + it('should produce same results as recursive version', () => { + expect(sumTreeIterative(tree)).toBe(sumTreeRecursive(tree)) + }) + + it('should sum all values in a tree', () => { + expect(sumTreeIterative(tree)).toBe(15) // 1+2+3+4+5 + }) + + it('should handle empty tree (null)', () => { + expect(sumTreeIterative(null)).toBe(0) + }) + + it('should handle single node tree', () => { + const single = { value: 42, left: null, right: null } + expect(sumTreeIterative(single)).toBe(42) + }) + + it('should handle left-only tree', () => { + const leftOnly = { + value: 1, + left: { + value: 2, + left: { value: 3, left: null, right: null }, + right: null + }, + right: null + } + expect(sumTreeIterative(leftOnly)).toBe(6) + }) + + it('should handle right-only tree', () => { + const rightOnly = { + value: 1, + left: null, + right: { + value: 2, + left: null, + right: { value: 3, left: null, right: null } + } + } + expect(sumTreeIterative(rightOnly)).toBe(6) + }) + }) + }) + + describe('Q&A Examples', () => { + describe('Array Length (Recursive)', () => { + // Exact implementation from MDX lines 899-910 + function arrayLength(arr) { + // Base case: empty array has length 0 + if (arr.length === 0) return 0 + + // Recursive case: 1 + length of the rest + return 1 + arrayLength(arr.slice(1)) + } + + it('should calculate array length recursively (MDX example)', () => { + expect(arrayLength([1, 2, 3, 4])).toBe(4) + }) + + it('should return 0 for empty array', () => { + expect(arrayLength([])).toBe(0) + }) + + it('should handle single element array', () => { + expect(arrayLength([42])).toBe(1) + }) + + it('should work with arrays of different types', () => { + expect(arrayLength(['a', 'b', 'c'])).toBe(3) + expect(arrayLength([{ a: 1 }, { b: 2 }])).toBe(2) + expect(arrayLength([1, 'two', { three: 3 }, [4]])).toBe(4) + }) + + it('should handle longer arrays', () => { + const longArray = Array.from({ length: 100 }, (_, i) => i) + expect(arrayLength(longArray)).toBe(100) + }) + }) + }) +}) diff --git a/tests/functions-execution/async-await/async-await.test.js b/tests/functions-execution/async-await/async-await.test.js new file mode 100644 index 00000000..50251aa9 --- /dev/null +++ b/tests/functions-execution/async-await/async-await.test.js @@ -0,0 +1,1042 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' + +describe('async/await', () => { + + // ============================================================ + // THE async KEYWORD + // ============================================================ + + describe('The async Keyword', () => { + it('should make a function return a Promise', () => { + // From: async function always returns a Promise + async function getValue() { + return 42 + } + + const result = getValue() + + expect(result).toBeInstanceOf(Promise) + }) + + it('should wrap return values in Promise.resolve()', async () => { + // From: return values are wrapped in Promise.resolve() + async function getValue() { + return 42 + } + + const result = await getValue() + + expect(result).toBe(42) + }) + + it('should convert thrown errors to rejected Promises', async () => { + // From: when you throw in an async function, it becomes a rejected Promise + async function failingFunction() { + throw new Error('Something went wrong!') + } + + await expect(failingFunction()).rejects.toThrow('Something went wrong!') + }) + + it('should not double-wrap returned Promises', async () => { + // From: return a Promise? No double-wrapping + async function getPromise() { + return Promise.resolve(42) + } + + const result = await getPromise() + + // If it double-wrapped, result would be a Promise, not 42 + expect(result).toBe(42) + expect(typeof result).toBe('number') + }) + + it('should work with async arrow functions', async () => { + // From: async arrow function + const getData = async () => { + return 'data' + } + + expect(await getData()).toBe('data') + }) + + it('should work with async methods in objects', async () => { + // From: async method in an object + const api = { + async fetchData() { + return 'fetched' + } + } + + expect(await api.fetchData()).toBe('fetched') + }) + + it('should work with async methods in classes', async () => { + // From: async method in a class + class DataService { + async getData() { + return 'class data' + } + } + + const service = new DataService() + expect(await service.getData()).toBe('class data') + }) + }) + + // ============================================================ + // THE await KEYWORD + // ============================================================ + + describe('The await Keyword', () => { + it('should pause execution until Promise resolves', async () => { + const order = [] + + async function example() { + order.push('before await') + await Promise.resolve() + order.push('after await') + } + + await example() + + expect(order).toEqual(['before await', 'after await']) + }) + + it('should return the resolved value of a Promise', async () => { + async function example() { + const value = await Promise.resolve(42) + return value + } + + expect(await example()).toBe(42) + }) + + it('should work with non-Promise values (though pointless)', async () => { + // From: awaiting a non-Promise value + async function example() { + const num = await 42 + return num + } + + expect(await example()).toBe(42) + }) + + it('should work with thenable objects', async () => { + // From: awaiting a thenable + const thenable = { + then(resolve) { + resolve('thenable value') + } + } + + async function example() { + return await thenable + } + + expect(await example()).toBe('thenable value') + }) + + it('should not block the main thread - other code runs while waiting', async () => { + // From: await pauses the function, not the thread + const order = [] + + async function slowOperation() { + order.push('Starting slow operation') + await Promise.resolve() + order.push('Slow operation complete') + } + + order.push('Before calling slowOperation') + const promise = slowOperation() + order.push('After calling slowOperation') + + // At this point, slowOperation is paused at await + expect(order).toEqual([ + 'Before calling slowOperation', + 'Starting slow operation', + 'After calling slowOperation' + ]) + + await promise + + expect(order).toEqual([ + 'Before calling slowOperation', + 'Starting slow operation', + 'After calling slowOperation', + 'Slow operation complete' + ]) + }) + }) + + // ============================================================ + // HOW await WORKS UNDER THE HOOD + // ============================================================ + + describe('How await Works Under the Hood', () => { + it('should run code before await synchronously', async () => { + // From: code before await is synchronous + const order = [] + + async function example() { + order.push('1. Before await') + await Promise.resolve() + order.push('2. After await') + } + + order.push('A. Before call') + example() + order.push('B. After call') + + // Before microtasks run + expect(order).toEqual([ + 'A. Before call', + '1. Before await', + 'B. After call' + ]) + + // Let microtasks run + await Promise.resolve() + + expect(order).toEqual([ + 'A. Before call', + '1. Before await', + 'B. After call', + '2. After await' + ]) + }) + + it('should treat code after await as a microtask', async () => { + // From: await splits the function diagram + const order = [] + + async function asyncFn() { + order.push('async start') + await Promise.resolve() + order.push('async after await') + } + + order.push('script start') + asyncFn() + order.push('script end') + + // Await hasn't resolved yet + expect(order).toEqual(['script start', 'async start', 'script end']) + + await Promise.resolve() + + expect(order).toEqual(['script start', 'async start', 'script end', 'async after await']) + }) + + it('should handle multiple await statements', async () => { + const order = [] + + async function multipleAwaits() { + order.push('start') + await Promise.resolve() + order.push('after first await') + await Promise.resolve() + order.push('after second await') + } + + multipleAwaits() + order.push('sync after call') + + expect(order).toEqual(['start', 'sync after call']) + + await Promise.resolve() + expect(order).toEqual(['start', 'sync after call', 'after first await']) + + await Promise.resolve() + expect(order).toEqual(['start', 'sync after call', 'after first await', 'after second await']) + }) + }) + + // ============================================================ + // ERROR HANDLING WITH try/catch + // ============================================================ + + describe('Error Handling with try/catch', () => { + it('should catch rejected Promises with try/catch', async () => { + // From: Basic try/catch pattern + async function fetchData() { + try { + await Promise.reject(new Error('Network error')) + return 'success' + } catch (error) { + return `caught: ${error.message}` + } + } + + expect(await fetchData()).toBe('caught: Network error') + }) + + it('should catch errors thrown in async functions', async () => { + async function mightFail(shouldFail) { + if (shouldFail) { + throw new Error('Failed!') + } + return 'Success' + } + + expect(await mightFail(false)).toBe('Success') + await expect(mightFail(true)).rejects.toThrow('Failed!') + }) + + it('should run finally block regardless of success or failure', async () => { + // From: The finally block + const results = [] + + async function withFinally(shouldFail) { + try { + if (shouldFail) { + throw new Error('error') + } + results.push('success') + } catch (error) { + results.push('caught') + } finally { + results.push('finally') + } + } + + await withFinally(false) + expect(results).toEqual(['success', 'finally']) + + results.length = 0 + await withFinally(true) + expect(results).toEqual(['caught', 'finally']) + }) + + it('should demonstrate the swallowed error mistake', async () => { + // From: The Trap - if you catch but don't re-throw, Promise resolves with undefined + async function swallowsError() { + try { + throw new Error('Oops') + } catch (error) { + console.error('Error:', error) + // Missing: throw error + } + } + + // This resolves (not rejects!) with undefined + const result = await swallowsError() + expect(result).toBeUndefined() + }) + + it('should propagate errors when re-thrown', async () => { + async function rethrowsError() { + try { + throw new Error('Oops') + } catch (error) { + throw error // Re-throw + } + } + + await expect(rethrowsError()).rejects.toThrow('Oops') + }) + + it('should catch errors from nested async calls', async () => { + // From: Interview Question 3 - Error Handling + async function inner() { + throw new Error('Oops!') + } + + async function outer() { + try { + await inner() + return 'success' + } catch (e) { + return `caught: ${e.message}` + } + } + + expect(await outer()).toBe('caught: Oops!') + }) + }) + + // ============================================================ + // SEQUENTIAL VS PARALLEL EXECUTION + // ============================================================ + + describe('Sequential vs Parallel Execution', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + it('should demonstrate slow sequential execution', async () => { + // From: The Problem - Unnecessary Sequential Execution + const delay = (ms, value) => new Promise(resolve => + setTimeout(() => resolve(value), ms) + ) + + async function sequential() { + const start = Date.now() + const a = await delay(100, 'a') + const b = await delay(100, 'b') + const c = await delay(100, 'c') + return { a, b, c, time: Date.now() - start } + } + + const promise = sequential() + + // Advance through all three delays + await vi.advanceTimersByTimeAsync(100) + await vi.advanceTimersByTimeAsync(100) + await vi.advanceTimersByTimeAsync(100) + + const result = await promise + expect(result.a).toBe('a') + expect(result.b).toBe('b') + expect(result.c).toBe('c') + expect(result.time).toBeGreaterThanOrEqual(300) // Sequential: 100+100+100 + }) + + it('should demonstrate fast parallel execution with Promise.all', async () => { + // From: The Solution - Promise.all for Parallel Execution + const delay = (ms, value) => new Promise(resolve => + setTimeout(() => resolve(value), ms) + ) + + async function parallel() { + const start = Date.now() + const [a, b, c] = await Promise.all([ + delay(100, 'a'), + delay(100, 'b'), + delay(100, 'c') + ]) + return { a, b, c, time: Date.now() - start } + } + + const promise = parallel() + + // All three start at once, so only need 100ms total + await vi.advanceTimersByTimeAsync(100) + + const result = await promise + expect(result.a).toBe('a') + expect(result.b).toBe('b') + expect(result.c).toBe('c') + expect(result.time).toBe(100) // Parallel: max(100,100,100) = 100 + }) + + it('should fail fast with Promise.all when any Promise rejects', async () => { + // From: Promise.all - fails fast + const results = await Promise.allSettled([ + Promise.resolve('success'), + Promise.reject(new Error('fail')), + Promise.resolve('also success') + ]) + + expect(results[0]).toEqual({ status: 'fulfilled', value: 'success' }) + expect(results[1].status).toBe('rejected') + expect(results[1].reason.message).toBe('fail') + expect(results[2]).toEqual({ status: 'fulfilled', value: 'also success' }) + }) + + it('should get all results with Promise.allSettled', async () => { + // From: Promise.allSettled - waits for all + const results = await Promise.allSettled([ + Promise.resolve('a'), + Promise.reject(new Error('b failed')), + Promise.resolve('c') + ]) + + const successful = results + .filter(r => r.status === 'fulfilled') + .map(r => r.value) + + const failed = results + .filter(r => r.status === 'rejected') + .map(r => r.reason.message) + + expect(successful).toEqual(['a', 'c']) + expect(failed).toEqual(['b failed']) + }) + }) + + // ============================================================ + // COMMON MISTAKES + // ============================================================ + + describe('Common Mistakes', () => { + it('Mistake #1: Forgetting await gives Promise instead of value', async () => { + // From: Without await, you get a Promise object instead of the resolved value + async function withoutAwait() { + const value = Promise.resolve(42) // Missing await! + return value + } + + async function withAwait() { + const value = await Promise.resolve(42) + return value + } + + const withoutResult = await withoutAwait() + const withResult = await withAwait() + + // Both eventually resolve to 42, but withoutAwait returns a Promise + expect(withoutResult).toBe(42) // Works because we await the function + expect(withResult).toBe(42) + }) + + it('Mistake #2: forEach does not wait for async callbacks', async () => { + // From: forEach doesn't wait for async callbacks + const order = [] + const items = [1, 2, 3] + + // This is the WRONG way + async function wrongWay() { + items.forEach(async (item) => { + await Promise.resolve() + order.push(item) + }) + order.push('done') + } + + await wrongWay() + // 'done' appears before the items because forEach doesn't wait + expect(order[0]).toBe('done') + + // Let microtasks complete + await Promise.resolve() + await Promise.resolve() + await Promise.resolve() + + expect(order).toEqual(['done', 1, 2, 3]) + }) + + it('Mistake #2 Fix: Use for...of for sequential processing', async () => { + // From: Use for...of for sequential + const order = [] + const items = [1, 2, 3] + + async function rightWay() { + for (const item of items) { + await Promise.resolve() + order.push(item) + } + order.push('done') + } + + await rightWay() + expect(order).toEqual([1, 2, 3, 'done']) + }) + + it('Mistake #2 Fix: Use Promise.all with map for parallel processing', async () => { + // From: Use Promise.all for parallel + const results = [] + const items = [1, 2, 3] + + async function parallelWay() { + await Promise.all( + items.map(async (item) => { + await Promise.resolve() + results.push(item) + }) + ) + results.push('done') + } + + await parallelWay() + // Items may be in any order (parallel), but 'done' is always last + expect(results).toContain(1) + expect(results).toContain(2) + expect(results).toContain(3) + expect(results[results.length - 1]).toBe('done') + }) + + it('Mistake #4: Not handling errors leads to unhandled rejections', async () => { + // From: Not Handling Errors + async function riskyOperation() { + throw new Error('Unhandled!') + } + + // Without error handling, this would be an unhandled rejection + await expect(riskyOperation()).rejects.toThrow('Unhandled!') + }) + }) + + // ============================================================ + // ADVANCED PATTERNS + // ============================================================ + + describe('Advanced Patterns', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + it('should implement retry with exponential backoff', async () => { + // From: Retry with Exponential Backoff + let attempts = 0 + + async function flakyOperation() { + attempts++ + if (attempts < 3) { + throw new Error('Temporary failure') + } + return 'success' + } + + async function withRetry(operation, retries = 3, backoff = 100) { + for (let attempt = 0; attempt < retries; attempt++) { + try { + return await operation() + } catch (error) { + if (attempt === retries - 1) throw error + await new Promise(resolve => setTimeout(resolve, backoff * Math.pow(2, attempt))) + } + } + } + + const promise = withRetry(flakyOperation, 3, 100) + + // First attempt fails, wait 100ms + await vi.advanceTimersByTimeAsync(0) + expect(attempts).toBe(1) + + // Second attempt after 100ms, fails, wait 200ms + await vi.advanceTimersByTimeAsync(100) + expect(attempts).toBe(2) + + // Third attempt after 200ms, succeeds + await vi.advanceTimersByTimeAsync(200) + + const result = await promise + expect(result).toBe('success') + expect(attempts).toBe(3) + }) + + it('should implement timeout wrapper', async () => { + // From: Timeout Wrapper + async function withTimeout(promise, ms) { + const timeout = new Promise((_, reject) => { + setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms) + }) + return Promise.race([promise, timeout]) + } + + // Test successful case + const fastPromise = withTimeout(Promise.resolve('fast'), 1000) + expect(await fastPromise).toBe('fast') + + // Test timeout case + const slowPromise = new Promise(resolve => setTimeout(() => resolve('slow'), 2000)) + const timeoutPromise = withTimeout(slowPromise, 100) + + // Advance time to trigger timeout + vi.advanceTimersByTime(100) + + await expect(timeoutPromise).rejects.toThrow('Timeout after 100ms') + }) + + it('should implement cancellation with AbortController', async () => { + // From: Cancellation with AbortController + async function fetchWithCancellation(signal) { + return new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => resolve('data'), 1000) + + signal.addEventListener('abort', () => { + clearTimeout(timeoutId) + reject(new DOMException('Aborted', 'AbortError')) + }) + }) + } + + const controller = new AbortController() + const promise = fetchWithCancellation(controller.signal) + + // Cancel before it completes + controller.abort() + + await expect(promise).rejects.toThrow('Aborted') + }) + + it('should convert callback API to async/await', async () => { + // From: Converting Callback APIs to async/await + function callbackApi(value, callback) { + setTimeout(() => { + if (value < 0) { + callback(new Error('Negative value')) + } else { + callback(null, value * 2) + } + }, 100) + } + + // Promisified version + function asyncApi(value) { + return new Promise((resolve, reject) => { + callbackApi(value, (err, result) => { + if (err) reject(err) + else resolve(result) + }) + }) + } + + // Test success + const successPromise = asyncApi(21) + await vi.advanceTimersByTimeAsync(100) + expect(await successPromise).toBe(42) + + // Test failure - must attach handler BEFORE advancing time + const failPromise = asyncApi(-1) + const failHandler = expect(failPromise).rejects.toThrow('Negative value') + await vi.advanceTimersByTimeAsync(100) + await failHandler + }) + }) + + // ============================================================ + // INTERVIEW QUESTIONS + // ============================================================ + + describe('Interview Questions', () => { + it('Question 1: What is the output order?', async () => { + // From: Interview Question 1 + const order = [] + + async function test() { + order.push('1') + await Promise.resolve() + order.push('2') + } + + order.push('A') + test() + order.push('B') + + // Before microtasks + expect(order).toEqual(['A', '1', 'B']) + + await Promise.resolve() + + // After microtasks + expect(order).toEqual(['A', '1', 'B', '2']) + }) + + it('Question 2: Sequential vs Parallel timing', async () => { + // From: Interview Question 2 + vi.useFakeTimers() + + function delay(ms) { + return new Promise(resolve => setTimeout(resolve, ms)) + } + + // Version A - Sequential + async function versionA() { + const start = Date.now() + await delay(100) + await delay(100) + return Date.now() - start + } + + // Version B - Parallel + async function versionB() { + const start = Date.now() + await Promise.all([delay(100), delay(100)]) + return Date.now() - start + } + + const promiseA = versionA() + await vi.advanceTimersByTimeAsync(200) + const timeA = await promiseA + + const promiseB = versionB() + await vi.advanceTimersByTimeAsync(100) + const timeB = await promiseB + + expect(timeA).toBe(200) // Sequential: 100 + 100 + expect(timeB).toBe(100) // Parallel: max(100, 100) + + vi.useRealTimers() + }) + + it('Question 4: The forEach trap', async () => { + // From: Interview Question 4 - The forEach Trap + vi.useFakeTimers() + + const order = [] + + async function processItems() { + const items = [1, 2, 3] + + items.forEach(async (item) => { + await new Promise(resolve => setTimeout(resolve, 100)) + order.push(item) + }) + + order.push('Done') + } + + processItems() + + // 'Done' appears immediately because forEach doesn't wait + expect(order).toEqual(['Done']) + + // After delays complete + await vi.advanceTimersByTimeAsync(100) + await Promise.resolve() + await Promise.resolve() + await Promise.resolve() + + expect(order).toEqual(['Done', 1, 2, 3]) + + vi.useRealTimers() + }) + + it('Question 5: Unnecessary await before return', async () => { + // From: Interview Question 5 - What is wrong here? + + // This await is unnecessary (but not wrong) + async function unnecessaryAwait() { + return await Promise.resolve(42) + } + + // This is equivalent and cleaner + async function noAwait() { + return Promise.resolve(42) + } + + expect(await unnecessaryAwait()).toBe(42) + expect(await noAwait()).toBe(42) + }) + + it('Question 6: Complex output order with async/await, Promises, and setTimeout', async () => { + // From: Test Your Knowledge Question 6 + vi.useFakeTimers() + + const order = [] + + order.push('1') + setTimeout(() => order.push('2'), 0) + Promise.resolve().then(() => order.push('3')) + + async function test() { + order.push('4') + await Promise.resolve() + order.push('5') + } + + test() + order.push('6') + + // Synchronous code completes: 1, 4, 6 + expect(order).toEqual(['1', '4', '6']) + + // Microtasks run: 3, 5 + await Promise.resolve() + await Promise.resolve() + expect(order).toEqual(['1', '4', '6', '3', '5']) + + // Macrotask runs: 2 + await vi.advanceTimersByTimeAsync(0) + expect(order).toEqual(['1', '4', '6', '3', '5', '2']) + + vi.useRealTimers() + }) + }) + + // ============================================================ + // ASYNC/AWAIT WITH PROMISES INTEROPERABILITY + // ============================================================ + + describe('async/await and Promises Interoperability', () => { + it('should work with .then() on async function results', async () => { + async function getData() { + return 42 + } + + const result = await getData().then(x => x * 2) + expect(result).toBe(84) + }) + + it('should work with .catch() on async function results', async () => { + async function failingFn() { + throw new Error('failed') + } + + const result = await failingFn().catch(err => `caught: ${err.message}`) + expect(result).toBe('caught: failed') + }) + + it('should allow mixing async/await and Promise chains', async () => { + async function step1() { + return 'step1' + } + + function step2(prev) { + return Promise.resolve(`${prev} -> step2`) + } + + async function step3(prev) { + return `${prev} -> step3` + } + + const result = await step1() + .then(step2) + .then(step3) + + expect(result).toBe('step1 -> step2 -> step3') + }) + + it('should handle Promise.race with async functions', async () => { + vi.useFakeTimers() + + async function fast() { + await new Promise(resolve => setTimeout(resolve, 50)) + return 'fast' + } + + async function slow() { + await new Promise(resolve => setTimeout(resolve, 200)) + return 'slow' + } + + const racePromise = Promise.race([fast(), slow()]) + + await vi.advanceTimersByTimeAsync(50) + + expect(await racePromise).toBe('fast') + + vi.useRealTimers() + }) + }) + + // ============================================================ + // EDGE CASES + // ============================================================ + + describe('Edge Cases', () => { + it('should handle async function that returns undefined', async () => { + async function returnsNothing() { + await Promise.resolve() + // No return statement + } + + const result = await returnsNothing() + expect(result).toBeUndefined() + }) + + it('should handle async function that returns null', async () => { + async function returnsNull() { + return null + } + + const result = await returnsNull() + expect(result).toBeNull() + }) + + it('should handle nested async functions', async () => { + async function outer() { + async function inner() { + return await Promise.resolve('inner value') + } + return await inner() + } + + expect(await outer()).toBe('inner value') + }) + + it('should handle async IIFE', async () => { + const result = await (async () => { + return 'IIFE result' + })() + + expect(result).toBe('IIFE result') + }) + + it('should handle await in conditional', async () => { + async function conditionalAwait(condition) { + if (condition) { + return await Promise.resolve('true branch') + } else { + return await Promise.resolve('false branch') + } + } + + expect(await conditionalAwait(true)).toBe('true branch') + expect(await conditionalAwait(false)).toBe('false branch') + }) + + it('should handle await in try-catch-finally', async () => { + const order = [] + + async function withTryCatchFinally() { + try { + order.push('try start') + await Promise.resolve() + order.push('try end') + throw new Error('test') + } catch (e) { + order.push('catch') + await Promise.resolve() + order.push('catch after await') + } finally { + order.push('finally') + await Promise.resolve() + order.push('finally after await') + } + } + + await withTryCatchFinally() + + expect(order).toEqual([ + 'try start', + 'try end', + 'catch', + 'catch after await', + 'finally', + 'finally after await' + ]) + }) + + it('should handle await in loop', async () => { + async function loopWithAwait() { + const results = [] + for (let i = 0; i < 3; i++) { + results.push(await Promise.resolve(i)) + } + return results + } + + expect(await loopWithAwait()).toEqual([0, 1, 2]) + }) + + it('should handle rejected Promise without await in try block', async () => { + // This is a subtle bug - the catch won't catch the rejection + // because the Promise is returned, not awaited + async function subtleBug() { + try { + return Promise.reject(new Error('not caught')) + } catch (e) { + return 'caught' + } + } + + // The error is NOT caught by the try-catch + await expect(subtleBug()).rejects.toThrow('not caught') + }) + + it('should catch rejected Promise when awaited in try block', async () => { + async function fixedVersion() { + try { + return await Promise.reject(new Error('is caught')) + } catch (e) { + return 'caught' + } + } + + // Now the error IS caught + expect(await fixedVersion()).toBe('caught') + }) + }) +}) diff --git a/tests/functions-execution/event-loop/event-loop.test.js b/tests/functions-execution/event-loop/event-loop.test.js new file mode 100644 index 00000000..0785716a --- /dev/null +++ b/tests/functions-execution/event-loop/event-loop.test.js @@ -0,0 +1,1319 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' + +describe('Event Loop, Timers and Scheduling', () => { + + // ============================================================ + // SYNCHRONOUS EXECUTION + // ============================================================ + + describe('Synchronous Execution', () => { + it('should execute code one line at a time in order', () => { + // From lines 99-104: JavaScript executes these ONE AT A TIME, in order + const order = [] + + order.push('First') // 1. This runs + order.push('Second') // 2. Then this + order.push('Third') // 3. Then this + + expect(order).toEqual(['First', 'Second', 'Third']) + }) + + it('should execute nested function calls correctly (multiply, square, printSquare)', () => { + // From lines 210-224: Call Stack example + function multiply(a, b) { + return a * b + } + + function square(n) { + return multiply(n, n) + } + + function printSquare(n) { + const result = square(n) + return result + } + + expect(printSquare(4)).toBe(16) + expect(square(5)).toBe(25) + expect(multiply(3, 4)).toBe(12) + }) + + it('should store objects and arrays in the heap', () => { + // From lines 243-245: Heap example + const user = { name: 'Alice' } // Object stored in heap + const numbers = [1, 2, 3] // Array stored in heap + + expect(user).toEqual({ name: 'Alice' }) + expect(numbers).toEqual([1, 2, 3]) + }) + }) + + // ============================================================ + // setTimeout BASICS + // ============================================================ + + describe('setTimeout Basics', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + it('should run callback after specified delay', async () => { + const callback = vi.fn() + + setTimeout(callback, 2000) + + expect(callback).not.toHaveBeenCalled() + + await vi.advanceTimersByTimeAsync(2000) + + expect(callback).toHaveBeenCalledTimes(1) + }) + + it('should pass arguments to the callback', async () => { + // From lines 562-566: Pass arguments to the callback + let result = '' + + setTimeout((name, greeting) => { + result = `${greeting}, ${name}!` + }, 1000, 'Alice', 'Hello') + + await vi.advanceTimersByTimeAsync(1000) + + expect(result).toBe('Hello, Alice!') + }) + + it('should cancel timeout with clearTimeout', async () => { + // From lines 569-577: Canceling a timeout + const callback = vi.fn() + + const timerId = setTimeout(callback, 5000) + + // Cancel it before it fires + clearTimeout(timerId) + + await vi.advanceTimersByTimeAsync(5000) + + expect(callback).not.toHaveBeenCalled() + }) + + it('should demonstrate the zero delay myth - setTimeout(fn, 0) does NOT run immediately', async () => { + // From lines 580-589: Zero delay myth + const order = [] + + order.push('A') + setTimeout(() => order.push('B'), 0) + order.push('C') + + // Before advancing timers, only sync code has run + expect(order).toEqual(['A', 'C']) + + await vi.advanceTimersByTimeAsync(0) + + // Output: A, C, B (NOT A, B, C!) + expect(order).toEqual(['A', 'C', 'B']) + }) + + it('should run synchronous code first, then setTimeout callback', async () => { + // From lines 313-323: Basic setTimeout example + const order = [] + + order.push('Start') + + setTimeout(() => { + order.push('Timeout') + }, 0) + + order.push('End') + + // Before microtasks/timers run + expect(order).toEqual(['Start', 'End']) + + await vi.advanceTimersByTimeAsync(0) + + // Output: Start, End, Timeout + expect(order).toEqual(['Start', 'End', 'Timeout']) + }) + + it('should run multiple setTimeout callbacks in order of their delays', async () => { + const order = [] + + setTimeout(() => order.push('200ms'), 200) + setTimeout(() => order.push('100ms'), 100) + setTimeout(() => order.push('300ms'), 300) + + await vi.advanceTimersByTimeAsync(300) + + expect(order).toEqual(['100ms', '200ms', '300ms']) + }) + + it('should run setTimeout callbacks with same delay in registration order', async () => { + const order = [] + + setTimeout(() => order.push('first'), 100) + setTimeout(() => order.push('second'), 100) + setTimeout(() => order.push('third'), 100) + + await vi.advanceTimersByTimeAsync(100) + + expect(order).toEqual(['first', 'second', 'third']) + }) + + it('should demonstrate the 4ms minimum delay after nested timeouts', async () => { + // From lines 601-615: After 5 nested timeouts, browsers enforce a minimum 4ms delay + // Note: Vitest fake timers don't enforce the 4ms minimum, so we test the pattern + const times = [] + let start = Date.now() + + function run() { + times.push(Date.now() - start) + if (times.length < 10) { + setTimeout(run, 0) + } + } + + setTimeout(run, 0) + + // Run all nested timeouts + await vi.runAllTimersAsync() + + // Should have 10 timestamps recorded + expect(times.length).toBe(10) + + // In fake timers, all execute at 0ms intervals + // In real browsers, after 5 nested calls, minimum becomes 4ms + // Pattern: [1, 1, 1, 1, 4, 9, 14, 19, 24, 29] approximately + }) + }) + + // ============================================================ + // DEBOUNCE PATTERN + // ============================================================ + + describe('Debounce Pattern', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + it('should cancel previous timeout when implementing debounce', async () => { + // From lines 1341-1349: Cancel previous timeout (debounce) + const searchResults = [] + let timeoutId + + function handleInput(value) { + clearTimeout(timeoutId) + timeoutId = setTimeout(() => { + searchResults.push(`search: ${value}`) + }, 300) + } + + // Simulate rapid typing + handleInput('a') + await vi.advanceTimersByTimeAsync(100) + + handleInput('ab') + await vi.advanceTimersByTimeAsync(100) + + handleInput('abc') + await vi.advanceTimersByTimeAsync(100) + + // At this point, 300ms hasn't passed since last input + expect(searchResults).toEqual([]) + + // Wait for debounce delay + await vi.advanceTimersByTimeAsync(300) + + // Only the last input should trigger a search + expect(searchResults).toEqual(['search: abc']) + }) + + it('should execute immediately if enough time passes between inputs', async () => { + const searchResults = [] + let timeoutId + + function handleInput(value) { + clearTimeout(timeoutId) + timeoutId = setTimeout(() => { + searchResults.push(`search: ${value}`) + }, 300) + } + + handleInput('first') + await vi.advanceTimersByTimeAsync(300) + expect(searchResults).toEqual(['search: first']) + + handleInput('second') + await vi.advanceTimersByTimeAsync(300) + expect(searchResults).toEqual(['search: first', 'search: second']) + }) + }) + + // ============================================================ + // SETINTERVAL WITH ASYNC PROBLEM + // ============================================================ + + describe('setInterval with Async Problem', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + it('should demonstrate overlapping requests with setInterval and async', async () => { + // From lines 1521-1526: If fetch takes longer than interval, multiple requests overlap + const requestsStarted = [] + const requestsCompleted = [] + let requestCount = 0 + + // Simulate a slow fetch that takes 1500ms + async function slowFetch() { + const id = ++requestCount + requestsStarted.push(`request ${id} started`) + await new Promise(resolve => setTimeout(resolve, 1500)) + requestsCompleted.push(`request ${id} completed`) + } + + // Start interval that fires every 1000ms + const intervalId = setInterval(async () => { + await slowFetch() + }, 1000) + + // After 1000ms: first request starts + await vi.advanceTimersByTimeAsync(1000) + await Promise.resolve() + expect(requestsStarted).toEqual(['request 1 started']) + expect(requestsCompleted).toEqual([]) + + // After 2000ms: second request starts (first still pending!) + await vi.advanceTimersByTimeAsync(1000) + await Promise.resolve() + expect(requestsStarted).toEqual(['request 1 started', 'request 2 started']) + expect(requestsCompleted).toEqual([]) // First request still not done + + // After 2500ms: first request completes + await vi.advanceTimersByTimeAsync(500) + await Promise.resolve() + expect(requestsCompleted).toEqual(['request 1 completed']) + + // Clean up + clearInterval(intervalId) + }) + + it('should demonstrate the fix using nested setTimeout for polling', async () => { + // From lines 1532-1539: Schedule next AFTER completion + const requestsStarted = [] + const requestsCompleted = [] + let requestCount = 0 + let isPolling = true + + // Simulate a slow fetch that takes 1500ms + async function slowFetch() { + const id = ++requestCount + requestsStarted.push(`request ${id} started`) + await new Promise(resolve => setTimeout(resolve, 1500)) + requestsCompleted.push(`request ${id} completed`) + } + + // Fixed polling pattern + async function poll() { + await slowFetch() + if (isPolling && requestCount < 3) { + setTimeout(poll, 1000) // Schedule next AFTER completion + } + } + + poll() + + // Request 1 starts immediately + await Promise.resolve() + expect(requestsStarted).toEqual(['request 1 started']) + + // After 1500ms: request 1 completes, then waits 1000ms before next + await vi.advanceTimersByTimeAsync(1500) + await Promise.resolve() + expect(requestsCompleted).toEqual(['request 1 completed']) + expect(requestsStarted.length).toBe(1) // No overlapping request! + + // After 2500ms (1500 + 1000): request 2 starts + await vi.advanceTimersByTimeAsync(1000) + await Promise.resolve() + expect(requestsStarted).toEqual(['request 1 started', 'request 2 started']) + + // After 4000ms (1500 + 1000 + 1500): request 2 completes + await vi.advanceTimersByTimeAsync(1500) + await Promise.resolve() + expect(requestsCompleted).toEqual(['request 1 completed', 'request 2 completed']) + + isPolling = false + }) + }) + + // ============================================================ + // PROMISES AND MICROTASKS + // ============================================================ + + describe('Promises and Microtasks', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + it('should run Promise.then() as a microtask', async () => { + const order = [] + + order.push('sync 1') + Promise.resolve().then(() => order.push('promise')) + order.push('sync 2') + + // Microtask hasn't run yet + expect(order).toEqual(['sync 1', 'sync 2']) + + // Let microtasks drain + await Promise.resolve() + + expect(order).toEqual(['sync 1', 'sync 2', 'promise']) + }) + + it('should run Promises BEFORE setTimeout (microtasks before macrotasks)', async () => { + // From lines 391-401: Promises vs setTimeout + const order = [] + + order.push('1') + + setTimeout(() => order.push('2'), 0) + + Promise.resolve().then(() => order.push('3')) + + order.push('4') + + // Let microtasks drain first + await Promise.resolve() + + // At this point: ['1', '4', '3'] + expect(order).toEqual(['1', '4', '3']) + + // Now let timers run + await vi.advanceTimersByTimeAsync(0) + + // Output: 1, 4, 3, 2 + expect(order).toEqual(['1', '4', '3', '2']) + }) + + it('should drain entire microtask queue before any macrotask', async () => { + // From lines 449-467: Nested Microtasks + const order = [] + + order.push('Start') + + Promise.resolve() + .then(() => { + order.push('Promise 1') + Promise.resolve().then(() => order.push('Promise 2')) + }) + + setTimeout(() => order.push('Timeout'), 0) + + order.push('End') + + // Let all microtasks drain + await Promise.resolve() + await Promise.resolve() // Need two ticks for nested promise + + expect(order).toEqual(['Start', 'End', 'Promise 1', 'Promise 2']) + + // Now let timers run + await vi.advanceTimersByTimeAsync(0) + + // Output: Start, End, Promise 1, Promise 2, Timeout + expect(order).toEqual(['Start', 'End', 'Promise 1', 'Promise 2', 'Timeout']) + }) + + it('should process newly added microtasks during microtask processing', async () => { + const order = [] + + Promise.resolve().then(() => { + order.push('first') + Promise.resolve().then(() => { + order.push('second') + Promise.resolve().then(() => { + order.push('third') + }) + }) + }) + + // Drain all microtasks - need multiple ticks for nested promises + await Promise.resolve() + await Promise.resolve() + await Promise.resolve() + await Promise.resolve() + + expect(order).toEqual(['first', 'second', 'third']) + }) + + it('should run queueMicrotask as a microtask', async () => { + const order = [] + + order.push('sync 1') + queueMicrotask(() => order.push('microtask')) + order.push('sync 2') + + await Promise.resolve() + + expect(order).toEqual(['sync 1', 'sync 2', 'microtask']) + }) + + it('should interleave Promise.resolve() and queueMicrotask in order', async () => { + const order = [] + + Promise.resolve().then(() => order.push('promise 1')) + queueMicrotask(() => order.push('queueMicrotask 1')) + Promise.resolve().then(() => order.push('promise 2')) + queueMicrotask(() => order.push('queueMicrotask 2')) + + await Promise.resolve() + await Promise.resolve() + + // Microtasks run in order they were queued + expect(order).toEqual(['promise 1', 'queueMicrotask 1', 'promise 2', 'queueMicrotask 2']) + }) + + it('should demonstrate that Promise.resolve() creates a microtask, not synchronous execution', async () => { + const order = [] + + const promise = Promise.resolve('value') + order.push('after Promise.resolve()') + + promise.then(value => order.push(`then: ${value}`)) + order.push('after .then()') + + await Promise.resolve() + + expect(order).toEqual([ + 'after Promise.resolve()', + 'after .then()', + 'then: value' + ]) + }) + }) + + // ============================================================ + // setInterval + // ============================================================ + + describe('setInterval', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + it('should run callback repeatedly at specified interval', async () => { + // From lines 649-662: Basic setInterval usage + let count = 0 + const results = [] + + const intervalId = setInterval(() => { + count++ + results.push(`Count: ${count}`) + + if (count >= 5) { + clearInterval(intervalId) + results.push('Done!') + } + }, 1000) + + // Advance through 5 intervals + await vi.advanceTimersByTimeAsync(5000) + + expect(results).toEqual([ + 'Count: 1', + 'Count: 2', + 'Count: 3', + 'Count: 4', + 'Count: 5', + 'Done!' + ]) + expect(count).toBe(5) + }) + + it('should stop running when clearInterval is called', async () => { + const callback = vi.fn() + + const intervalId = setInterval(callback, 100) + + await vi.advanceTimersByTimeAsync(250) + expect(callback).toHaveBeenCalledTimes(2) + + clearInterval(intervalId) + + await vi.advanceTimersByTimeAsync(500) + // Should still be 2, not more + expect(callback).toHaveBeenCalledTimes(2) + }) + + it('should pass arguments to the interval callback', async () => { + const results = [] + + const intervalId = setInterval((prefix, suffix) => { + results.push(`${prefix}test${suffix}`) + }, 100, '[', ']') + + await vi.advanceTimersByTimeAsync(300) + + clearInterval(intervalId) + + expect(results).toEqual(['[test]', '[test]', '[test]']) + }) + }) + + // ============================================================ + // NESTED setTimeout (preciseInterval pattern) + // ============================================================ + + describe('Nested setTimeout (preciseInterval pattern)', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + it('should implement preciseInterval with nested setTimeout', async () => { + // From lines 695-706: Nested setTimeout guarantees delay BETWEEN executions + const results = [] + let callCount = 0 + + function preciseInterval(callback, delay) { + function tick() { + callback() + if (callCount < 3) { + setTimeout(tick, delay) + } + } + setTimeout(tick, delay) + } + + preciseInterval(() => { + callCount++ + results.push(`tick ${callCount}`) + }, 1000) + + await vi.advanceTimersByTimeAsync(3000) + + expect(results).toEqual(['tick 1', 'tick 2', 'tick 3']) + }) + + it('should schedule next timeout only after callback completes', async () => { + const timestamps = [] + let count = 0 + + function recursiveTimeout() { + timestamps.push(Date.now()) + count++ + if (count < 3) { + setTimeout(recursiveTimeout, 100) + } + } + + setTimeout(recursiveTimeout, 100) + + await vi.advanceTimersByTimeAsync(300) + + expect(timestamps.length).toBe(3) + // Each timestamp should be 100ms apart + expect(timestamps[1] - timestamps[0]).toBe(100) + expect(timestamps[2] - timestamps[1]).toBe(100) + }) + }) + + // ============================================================ + // async/await + // ============================================================ + + describe('async/await', () => { + it('should run code before await synchronously', async () => { + // From lines 955-964: async/await ordering + const order = [] + + async function foo() { + order.push('foo start') + await Promise.resolve() + order.push('foo end') + } + + order.push('script start') + foo() + order.push('script end') + + // At this point, foo has paused at await + expect(order).toEqual(['script start', 'foo start', 'script end']) + + // Let microtasks drain + await Promise.resolve() + + // Output: script start, foo start, script end, foo end + expect(order).toEqual(['script start', 'foo start', 'script end', 'foo end']) + }) + + it('should treat code after await as a microtask', async () => { + const order = [] + + async function asyncFn() { + order.push('async start') + await Promise.resolve() + order.push('async after await') + } + + order.push('before call') + asyncFn() + order.push('after call') + + // Await hasn't resolved yet + expect(order).toEqual(['before call', 'async start', 'after call']) + + await Promise.resolve() + + expect(order).toEqual(['before call', 'async start', 'after call', 'async after await']) + }) + + it('should handle multiple await statements', async () => { + const order = [] + + async function multipleAwaits() { + order.push('start') + await Promise.resolve() + order.push('after first await') + await Promise.resolve() + order.push('after second await') + } + + multipleAwaits() + order.push('sync after call') + + expect(order).toEqual(['start', 'sync after call']) + + await Promise.resolve() + expect(order).toEqual(['start', 'sync after call', 'after first await']) + + await Promise.resolve() + expect(order).toEqual(['start', 'sync after call', 'after first await', 'after second await']) + }) + + it('should handle async functions returning values', async () => { + async function getValue() { + await Promise.resolve() + return 42 + } + + const result = await getValue() + expect(result).toBe(42) + }) + + it('should handle async functions with try/catch', async () => { + async function mightFail(shouldFail) { + await Promise.resolve() + if (shouldFail) { + throw new Error('Failed!') + } + return 'Success' + } + + await expect(mightFail(false)).resolves.toBe('Success') + await expect(mightFail(true)).rejects.toThrow('Failed!') + }) + }) + + // ============================================================ + // INTERVIEW QUESTIONS + // ============================================================ + + describe('Interview Questions', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + it('Question 1: Basic Output Order - should output 1, 4, 3, 2', async () => { + // From lines 900-918 + const order = [] + + order.push('1') + setTimeout(() => order.push('2'), 0) + Promise.resolve().then(() => order.push('3')) + order.push('4') + + // Drain microtasks + await Promise.resolve() + + expect(order).toEqual(['1', '4', '3']) + + // Drain macrotasks + await vi.advanceTimersByTimeAsync(0) + + expect(order).toEqual(['1', '4', '3', '2']) + }) + + it('Question 2: Nested Promises and Timeouts - should output sync, promise 1, promise 2, timeout 1, timeout 2', async () => { + // From lines 921-951 + const order = [] + + setTimeout(() => order.push('timeout 1'), 0) + + Promise.resolve().then(() => { + order.push('promise 1') + Promise.resolve().then(() => order.push('promise 2')) + }) + + setTimeout(() => order.push('timeout 2'), 0) + + order.push('sync') + + // Sync done + expect(order).toEqual(['sync']) + + // Drain microtasks (including nested ones) + await Promise.resolve() + await Promise.resolve() + + expect(order).toEqual(['sync', 'promise 1', 'promise 2']) + + // Drain macrotasks + await vi.advanceTimersByTimeAsync(0) + + expect(order).toEqual(['sync', 'promise 1', 'promise 2', 'timeout 1', 'timeout 2']) + }) + + it('Question 3: async/await Ordering - should output script start, foo start, script end, foo end', async () => { + // From lines 953-981 + const order = [] + + async function foo() { + order.push('foo start') + await Promise.resolve() + order.push('foo end') + } + + order.push('script start') + foo() + order.push('script end') + + await Promise.resolve() + + expect(order).toEqual(['script start', 'foo start', 'script end', 'foo end']) + }) + + it('Question 4a: setTimeout in a loop with var - should output 3, 3, 3', async () => { + // From lines 985-997 + const order = [] + + for (var i = 0; i < 3; i++) { + setTimeout(() => order.push(i), 0) + } + + await vi.advanceTimersByTimeAsync(0) + + // All callbacks see i = 3 because var is function-scoped + expect(order).toEqual([3, 3, 3]) + }) + + it('Question 4b: setTimeout in a loop with let - should output 0, 1, 2', async () => { + // From lines 999-1004 + const order = [] + + for (let i = 0; i < 3; i++) { + setTimeout(() => order.push(i), 0) + } + + await vi.advanceTimersByTimeAsync(0) + + // Each callback has its own i because let is block-scoped + expect(order).toEqual([0, 1, 2]) + }) + + it('Question 4c: setTimeout in a loop with closure fix - should output 0, 1, 2', async () => { + // From lines 1007-1015 + const order = [] + + for (var i = 0; i < 3; i++) { + ((j) => { + setTimeout(() => order.push(j), 0) + })(i) + } + + await vi.advanceTimersByTimeAsync(0) + + // Each IIFE captures the current value of i + expect(order).toEqual([0, 1, 2]) + }) + + it('Question 6: Microtask scheduling - microtask should run', async () => { + // From lines 1051-1077 (simplified - not infinite) + const order = [] + let count = 0 + + function scheduleMicrotask() { + Promise.resolve().then(() => { + count++ + order.push(`microtask ${count}`) + if (count < 3) { + scheduleMicrotask() + } + }) + } + + scheduleMicrotask() + + // Drain all microtasks + await Promise.resolve() + await Promise.resolve() + await Promise.resolve() + + expect(order).toEqual(['microtask 1', 'microtask 2', 'microtask 3']) + }) + + it('Misconception 1: setTimeout(fn, 0) does NOT run immediately - should output sync, promise, timeout', async () => { + // From lines 1084-1096 + const order = [] + + setTimeout(() => order.push('timeout'), 0) + Promise.resolve().then(() => order.push('promise')) + order.push('sync') + + await Promise.resolve() + await vi.advanceTimersByTimeAsync(0) + + // Output: sync, promise, timeout (NOT sync, timeout, promise) + expect(order).toEqual(['sync', 'promise', 'timeout']) + }) + + it('Test Your Knowledge Q3: Complex ordering - should output E, B, C, A, D', async () => { + // From lines 1487-1504 + const order = [] + + setTimeout(() => order.push('A'), 0) + Promise.resolve().then(() => order.push('B')) + Promise.resolve().then(() => { + order.push('C') + setTimeout(() => order.push('D'), 0) + }) + order.push('E') + + // Drain microtasks + await Promise.resolve() + await Promise.resolve() + + expect(order).toEqual(['E', 'B', 'C']) + + // Drain macrotasks + await vi.advanceTimersByTimeAsync(0) + + expect(order).toEqual(['E', 'B', 'C', 'A', 'D']) + }) + }) + + // ============================================================ + // COMMON PATTERNS + // ============================================================ + + describe('Common Patterns', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + it('this binding: regular function loses this context', async () => { + // From lines 1354-1363 + const obj = { + name: 'Alice', + greet() { + return new Promise(resolve => { + setTimeout(function() { + // 'this' is undefined in strict mode (or global in non-strict) + resolve(this?.name) + }, 100) + }) + } + } + + const resultPromise = obj.greet() + await vi.advanceTimersByTimeAsync(100) + const result = await resultPromise + + expect(result).toBeUndefined() + }) + + it('this binding: arrow function preserves this context', async () => { + // From lines 1365-1373 + const obj = { + name: 'Alice', + greet() { + return new Promise(resolve => { + setTimeout(() => { + resolve(this.name) + }, 100) + }) + } + } + + const resultPromise = obj.greet() + await vi.advanceTimersByTimeAsync(100) + const result = await resultPromise + + expect(result).toBe('Alice') + }) + + it('this binding: bind() preserves this context', async () => { + // From lines 1375-1383 + const obj = { + name: 'Alice', + greet() { + return new Promise(resolve => { + setTimeout(function() { + resolve(this.name) + }.bind(this), 100) + }) + } + } + + const resultPromise = obj.greet() + await vi.advanceTimersByTimeAsync(100) + const result = await resultPromise + + expect(result).toBe('Alice') + }) + + it('closure in loop: var creates shared reference', async () => { + // From lines 1388-1393 + const results = [] + + for (var i = 0; i < 3; i++) { + setTimeout(() => results.push(i), 100) + } + + await vi.advanceTimersByTimeAsync(100) + + // Output: 3, 3, 3 + expect(results).toEqual([3, 3, 3]) + }) + + it('closure in loop: let creates new binding per iteration', async () => { + // From lines 1395-1399 + const results = [] + + for (let i = 0; i < 3; i++) { + setTimeout(() => results.push(i), 100) + } + + await vi.advanceTimersByTimeAsync(100) + + // Output: 0, 1, 2 + expect(results).toEqual([0, 1, 2]) + }) + + it('closure in loop: setTimeout third argument passes value', async () => { + // From lines 1401-1405 + const results = [] + + for (var i = 0; i < 3; i++) { + setTimeout((j) => results.push(j), 100, i) + } + + await vi.advanceTimersByTimeAsync(100) + + // Output: 0, 1, 2 + expect(results).toEqual([0, 1, 2]) + }) + + it('should implement chunking with setTimeout', async () => { + // From lines 1196-1215 + const processed = [] + const items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + + function processInChunks(items, process, chunkSize = 3) { + let index = 0 + + function doChunk() { + const end = Math.min(index + chunkSize, items.length) + + for (; index < end; index++) { + process(items[index]) + } + + if (index < items.length) { + setTimeout(doChunk, 0) + } + } + + doChunk() + } + + processInChunks(items, item => processed.push(item), 3) + + // First chunk runs synchronously + expect(processed).toEqual([1, 2, 3]) + + // Run all remaining timers + await vi.runAllTimersAsync() + + expect(processed).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + }) + + it('should implement async polling with nested setTimeout', async () => { + // From lines 1532-1540 + const results = [] + let pollCount = 0 + + async function poll() { + // Simulate async fetch + const data = await Promise.resolve(`data ${++pollCount}`) + results.push(data) + + if (pollCount < 3) { + setTimeout(poll, 1000) + } + } + + poll() + + // First poll completes immediately (Promise.resolve) + await Promise.resolve() + expect(results).toEqual(['data 1']) + + // Second poll after 1000ms + await vi.advanceTimersByTimeAsync(1000) + await Promise.resolve() + expect(results).toEqual(['data 1', 'data 2']) + + // Third poll after another 1000ms + await vi.advanceTimersByTimeAsync(1000) + await Promise.resolve() + expect(results).toEqual(['data 1', 'data 2', 'data 3']) + }) + }) + + // ============================================================ + // YIELDING TO EVENT LOOP + // ============================================================ + + describe('Yielding to Event Loop', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + it('should yield with setTimeout(resolve, 0)', async () => { + // From lines 1547-1548 + const order = [] + + order.push('before yield') + + // Create the promise but don't await yet + const yieldPromise = new Promise(resolve => setTimeout(resolve, 0)) + + // Advance timers to resolve the setTimeout + await vi.advanceTimersByTimeAsync(0) + + // Now await the resolved promise + await yieldPromise + + order.push('after yield') + + expect(order).toEqual(['before yield', 'after yield']) + }) + + it('should yield with queueMicrotask', async () => { + // From lines 1550-1551 + const order = [] + + order.push('before yield') + + await new Promise(resolve => queueMicrotask(resolve)) + + order.push('after yield') + + expect(order).toEqual(['before yield', 'after yield']) + }) + + it('should demonstrate difference between setTimeout and queueMicrotask yields', async () => { + const order = [] + + // Schedule a setTimeout callback + setTimeout(() => order.push('timeout'), 0) + + // Yield with queueMicrotask - runs before timeout + await new Promise(resolve => queueMicrotask(resolve)) + order.push('after queueMicrotask yield') + + // Timeout hasn't run yet + expect(order).toEqual(['after queueMicrotask yield']) + + // Now let timeout run + await vi.advanceTimersByTimeAsync(0) + expect(order).toEqual(['after queueMicrotask yield', 'timeout']) + }) + }) + + // ============================================================ + // EDGE CASES + // ============================================================ + + describe('Edge Cases', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + it('should handle Promise.resolve() vs new Promise()', async () => { + const order = [] + + // Both create microtasks + Promise.resolve().then(() => order.push('Promise.resolve')) + new Promise(resolve => resolve()).then(() => order.push('new Promise')) + + await Promise.resolve() + await Promise.resolve() + + expect(order).toEqual(['Promise.resolve', 'new Promise']) + }) + + it('should handle setTimeout with 0 vs undefined delay', async () => { + const order = [] + + setTimeout(() => order.push('explicit 0'), 0) + setTimeout(() => order.push('undefined delay')) + + await vi.advanceTimersByTimeAsync(0) + + // Both should run (undefined defaults to 0) + expect(order).toEqual(['explicit 0', 'undefined delay']) + }) + + it('should handle clearTimeout with invalid ID', () => { + // Should not throw + expect(() => clearTimeout(undefined)).not.toThrow() + expect(() => clearTimeout(null)).not.toThrow() + expect(() => clearTimeout(999999)).not.toThrow() + }) + + it('should handle clearInterval with invalid ID', () => { + // Should not throw + expect(() => clearInterval(undefined)).not.toThrow() + expect(() => clearInterval(null)).not.toThrow() + expect(() => clearInterval(999999)).not.toThrow() + }) + + it('should handle promise rejection in microtask', async () => { + const order = [] + + Promise.resolve() + .then(() => { + order.push('then 1') + throw new Error('error') + }) + .catch(() => order.push('catch')) + .then(() => order.push('then after catch')) + + await Promise.resolve() + await Promise.resolve() + await Promise.resolve() + + expect(order).toEqual(['then 1', 'catch', 'then after catch']) + }) + + it('should handle nested setTimeout with different delays', async () => { + const order = [] + + setTimeout(() => { + order.push('outer') + setTimeout(() => order.push('inner'), 50) + }, 100) + + await vi.advanceTimersByTimeAsync(100) + expect(order).toEqual(['outer']) + + await vi.advanceTimersByTimeAsync(50) + expect(order).toEqual(['outer', 'inner']) + }) + + it('should handle multiple Promise.then chains', async () => { + const order = [] + + const p = Promise.resolve() + + p.then(() => order.push('chain 1')) + p.then(() => order.push('chain 2')) + p.then(() => order.push('chain 3')) + + await Promise.resolve() + + // All chains from same promise run in order + expect(order).toEqual(['chain 1', 'chain 2', 'chain 3']) + }) + + it('should handle async function that returns immediately', async () => { + const order = [] + + async function immediate() { + order.push('inside async') + return 'result' + } + + order.push('before') + const promise = immediate() + order.push('after') + + // async function body runs synchronously until first await + expect(order).toEqual(['before', 'inside async', 'after']) + + const result = await promise + expect(result).toBe('result') + }) + + it('should handle Promise.all with microtask ordering', async () => { + const order = [] + + Promise.all([ + Promise.resolve().then(() => order.push('p1')), + Promise.resolve().then(() => order.push('p2')), + Promise.resolve().then(() => order.push('p3')) + ]).then(() => order.push('all done')) + + // First tick: individual promises resolve + await Promise.resolve() + expect(order).toEqual(['p1', 'p2', 'p3']) + + // Second tick: Promise.all sees all resolved + await Promise.resolve() + // Third tick: .then() callback runs + await Promise.resolve() + expect(order).toEqual(['p1', 'p2', 'p3', 'all done']) + }) + }) +}) diff --git a/tests/functions-execution/generators-iterators/generators-iterators.test.js b/tests/functions-execution/generators-iterators/generators-iterators.test.js new file mode 100644 index 00000000..030dc66d --- /dev/null +++ b/tests/functions-execution/generators-iterators/generators-iterators.test.js @@ -0,0 +1,593 @@ +import { describe, it, expect } from 'vitest' + +describe('Generators & Iterators', () => { + describe('Basic Iterator Protocol', () => { + it('should follow the iterator protocol with { value, done }', () => { + function createIterator(arr) { + let index = 0 + return { + next() { + if (index < arr.length) { + return { value: arr[index++], done: false } + } + return { value: undefined, done: true } + } + } + } + + const iterator = createIterator([1, 2, 3]) + + expect(iterator.next()).toEqual({ value: 1, done: false }) + expect(iterator.next()).toEqual({ value: 2, done: false }) + expect(iterator.next()).toEqual({ value: 3, done: false }) + expect(iterator.next()).toEqual({ value: undefined, done: true }) + }) + + it('should allow accessing array iterator via Symbol.iterator', () => { + const arr = [10, 20, 30] + const iterator = arr[Symbol.iterator]() + + expect(iterator.next()).toEqual({ value: 10, done: false }) + expect(iterator.next()).toEqual({ value: 20, done: false }) + expect(iterator.next()).toEqual({ value: 30, done: false }) + expect(iterator.next()).toEqual({ value: undefined, done: true }) + }) + }) + + describe('Basic Generator Syntax', () => { + it('should create a generator function with function*', () => { + function* simpleGenerator() { + yield 1 + yield 2 + yield 3 + } + + const gen = simpleGenerator() + + expect(gen.next()).toEqual({ value: 1, done: false }) + expect(gen.next()).toEqual({ value: 2, done: false }) + expect(gen.next()).toEqual({ value: 3, done: false }) + expect(gen.next()).toEqual({ value: undefined, done: true }) + }) + + it('should not execute until .next() is called', () => { + let executed = false + + function* trackExecution() { + executed = true + yield 'done' + } + + const gen = trackExecution() + expect(executed).toBe(false) // Not executed yet! + + gen.next() + expect(executed).toBe(true) // Now it's executed + }) + + it('should work with for...of loops', () => { + function* colors() { + yield 'red' + yield 'green' + yield 'blue' + } + + const result = [] + for (const color of colors()) { + result.push(color) + } + + expect(result).toEqual(['red', 'green', 'blue']) + }) + + it('should work with spread operator', () => { + function* numbers() { + yield 1 + yield 2 + yield 3 + } + + expect([...numbers()]).toEqual([1, 2, 3]) + }) + }) + + describe('yield vs return', () => { + it('should pause execution at yield and allow resuming', () => { + const executionOrder = [] + + function* trackOrder() { + executionOrder.push('before first yield') + yield 'A' + executionOrder.push('after first yield') + yield 'B' + executionOrder.push('after second yield') + } + + const gen = trackOrder() + + expect(executionOrder).toEqual([]) + + gen.next() + expect(executionOrder).toEqual(['before first yield']) + + gen.next() + expect(executionOrder).toEqual(['before first yield', 'after first yield']) + + gen.next() + expect(executionOrder).toEqual([ + 'before first yield', + 'after first yield', + 'after second yield' + ]) + }) + + it('should mark done: true on return', () => { + function* withReturn() { + yield 'A' + return 'B' + yield 'C' // This never executes + } + + const gen = withReturn() + + expect(gen.next()).toEqual({ value: 'A', done: false }) + expect(gen.next()).toEqual({ value: 'B', done: true }) + expect(gen.next()).toEqual({ value: undefined, done: true }) + }) + + it('should NOT include return value in for...of', () => { + function* withReturn() { + yield 'A' + yield 'B' + return 'C' // Not included in iteration! + } + + expect([...withReturn()]).toEqual(['A', 'B']) // No 'C'! + }) + }) + + describe('yield* delegation', () => { + it('should delegate to another iterable', () => { + function* inner() { + yield 'a' + yield 'b' + } + + function* outer() { + yield 1 + yield* inner() + yield 2 + } + + expect([...outer()]).toEqual([1, 'a', 'b', 2]) + }) + + it('should delegate to arrays', () => { + function* withArray() { + yield 'start' + yield* [1, 2, 3] + yield 'end' + } + + expect([...withArray()]).toEqual(['start', 1, 2, 3, 'end']) + }) + + it('should flatten nested arrays recursively', () => { + function* flatten(arr) { + for (const item of arr) { + if (Array.isArray(item)) { + yield* flatten(item) + } else { + yield item + } + } + } + + const nested = [1, [2, 3, [4, 5]], 6] + expect([...flatten(nested)]).toEqual([1, 2, 3, 4, 5, 6]) + }) + }) + + describe('Passing values to generators', () => { + it('should receive values via .next(value)', () => { + function* adder() { + const a = yield 'Enter first number' + const b = yield 'Enter second number' + yield a + b + } + + const gen = adder() + + expect(gen.next().value).toBe('Enter first number') + expect(gen.next(10).value).toBe('Enter second number') + expect(gen.next(5).value).toBe(15) + }) + + it('should ignore value passed to first .next()', () => { + function* capture() { + const first = yield 'ready' + yield first + } + + const gen = capture() + + // First .next() value is ignored because no yield is waiting + gen.next('IGNORED') + expect(gen.next('captured').value).toBe('captured') + }) + }) + + describe('Symbol.iterator - Custom Iterables', () => { + it('should make object iterable with Symbol.iterator', () => { + const myCollection = { + items: ['apple', 'banana', 'cherry'], + [Symbol.iterator]() { + let index = 0 + const items = this.items + return { + next() { + if (index < items.length) { + return { value: items[index++], done: false } + } + return { value: undefined, done: true } + } + } + } + } + + expect([...myCollection]).toEqual(['apple', 'banana', 'cherry']) + }) + + it('should make object iterable with generator', () => { + const myCollection = { + items: [1, 2, 3], + *[Symbol.iterator]() { + yield* this.items + } + } + + const result = [] + for (const item of myCollection) { + result.push(item) + } + + expect(result).toEqual([1, 2, 3]) + }) + + it('should create an iterable Range class', () => { + class Range { + constructor(start, end, step = 1) { + this.start = start + this.end = end + this.step = step + } + + *[Symbol.iterator]() { + for (let i = this.start; i <= this.end; i += this.step) { + yield i + } + } + } + + expect([...new Range(1, 5)]).toEqual([1, 2, 3, 4, 5]) + expect([...new Range(0, 10, 2)]).toEqual([0, 2, 4, 6, 8, 10]) + expect([...new Range(5, 1)]).toEqual([]) // Empty when start > end + }) + }) + + describe('Lazy Evaluation', () => { + it('should compute values on demand', () => { + let computeCount = 0 + + function* lazyComputation() { + while (true) { + computeCount++ + yield computeCount + } + } + + const gen = lazyComputation() + + expect(computeCount).toBe(0) // Nothing computed yet + + gen.next() + expect(computeCount).toBe(1) + + gen.next() + expect(computeCount).toBe(2) + + // Only computed twice, not infinitely + }) + + it('should handle infinite sequences safely with take()', () => { + function* naturalNumbers() { + let n = 1 + while (true) { + yield n++ + } + } + + function* take(n, iterable) { + let count = 0 + for (const item of iterable) { + if (count >= n) return + yield item + count++ + } + } + + expect([...take(5, naturalNumbers())]).toEqual([1, 2, 3, 4, 5]) + }) + + it('should generate Fibonacci sequence lazily', () => { + function* fibonacci() { + let prev = 0 + let curr = 1 + + while (true) { + yield curr + const next = prev + curr + prev = curr + curr = next + } + } + + function* take(n, iterable) { + let count = 0 + for (const item of iterable) { + if (count >= n) return + yield item + count++ + } + } + + expect([...take(10, fibonacci())]).toEqual([ + 1, 1, 2, 3, 5, 8, 13, 21, 34, 55 + ]) + }) + }) + + describe('Common Patterns', () => { + it('should create a unique ID generator', () => { + function* createIdGenerator(prefix = 'id') { + let id = 1 + while (true) { + yield `${prefix}_${id++}` + } + } + + const userIds = createIdGenerator('user') + const orderIds = createIdGenerator('order') + + expect(userIds.next().value).toBe('user_1') + expect(userIds.next().value).toBe('user_2') + expect(orderIds.next().value).toBe('order_1') + expect(userIds.next().value).toBe('user_3') + expect(orderIds.next().value).toBe('order_2') + }) + + it('should chunk arrays into batches', () => { + function* chunk(array, size) { + for (let i = 0; i < array.length; i += size) { + yield array.slice(i, i + size) + } + } + + const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + + expect([...chunk(data, 3)]).toEqual([ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + [10] + ]) + }) + + it('should implement filter and map with generators', () => { + function* filter(iterable, predicate) { + for (const item of iterable) { + if (predicate(item)) { + yield item + } + } + } + + function* map(iterable, transform) { + for (const item of iterable) { + yield transform(item) + } + } + + function* range(start, end) { + for (let i = start; i <= end; i++) { + yield i + } + } + + // Pipeline: 1-10 -> filter evens -> double them + const result = map( + filter(range(1, 10), n => n % 2 === 0), + n => n * 2 + ) + + expect([...result]).toEqual([4, 8, 12, 16, 20]) + }) + + it('should implement a simple state machine', () => { + function* trafficLight() { + while (true) { + yield 'green' + yield 'yellow' + yield 'red' + } + } + + const light = trafficLight() + + expect(light.next().value).toBe('green') + expect(light.next().value).toBe('yellow') + expect(light.next().value).toBe('red') + expect(light.next().value).toBe('green') // Cycles back + expect(light.next().value).toBe('yellow') + }) + + it('should traverse a tree structure', () => { + function* traverseTree(node) { + yield node.value + + if (node.children) { + for (const child of node.children) { + yield* traverseTree(child) + } + } + } + + const tree = { + value: 'root', + children: [ + { + value: 'child1', + children: [{ value: 'grandchild1' }, { value: 'grandchild2' }] + }, + { + value: 'child2', + children: [{ value: 'grandchild3' }] + } + ] + } + + expect([...traverseTree(tree)]).toEqual([ + 'root', + 'child1', + 'grandchild1', + 'grandchild2', + 'child2', + 'grandchild3' + ]) + }) + }) + + describe('Async Generators', () => { + it('should create async generators with async function*', async () => { + async function* asyncNumbers() { + yield await Promise.resolve(1) + yield await Promise.resolve(2) + yield await Promise.resolve(3) + } + + const results = [] + for await (const num of asyncNumbers()) { + results.push(num) + } + + expect(results).toEqual([1, 2, 3]) + }) + + it('should handle delayed async values', async () => { + const delay = ms => new Promise(resolve => setTimeout(resolve, ms)) + + async function* delayedNumbers() { + await delay(10) + yield 1 + await delay(10) + yield 2 + await delay(10) + yield 3 + } + + const results = [] + for await (const num of delayedNumbers()) { + results.push(num) + } + + expect(results).toEqual([1, 2, 3]) + }) + + it('should simulate paginated API fetching', async () => { + // Mock paginated data + const mockPages = [ + { items: ['a', 'b'], hasNextPage: true }, + { items: ['c', 'd'], hasNextPage: true }, + { items: ['e'], hasNextPage: false } + ] + + async function* fetchAllPages() { + let page = 0 + let hasMore = true + + while (hasMore) { + // Simulate API call + const data = await Promise.resolve(mockPages[page]) + yield data.items + hasMore = data.hasNextPage + page++ + } + } + + const allItems = [] + for await (const pageItems of fetchAllPages()) { + allItems.push(...pageItems) + } + + expect(allItems).toEqual(['a', 'b', 'c', 'd', 'e']) + }) + }) + + describe('Generator Exhaustion', () => { + it('should only be iterable once', () => { + function* nums() { + yield 1 + yield 2 + } + + const gen = nums() + + expect([...gen]).toEqual([1, 2]) + expect([...gen]).toEqual([]) // Exhausted! + }) + + it('should create fresh generator for each iteration', () => { + function* nums() { + yield 1 + yield 2 + } + + expect([...nums()]).toEqual([1, 2]) + expect([...nums()]).toEqual([1, 2]) // Fresh generator each time + }) + }) + + describe('Error Handling', () => { + it('should allow throwing errors into generator with .throw()', () => { + function* gen() { + try { + yield 'A' + yield 'B' + } catch (e) { + yield `Error: ${e.message}` + } + } + + const g = gen() + + expect(g.next().value).toBe('A') + expect(g.throw(new Error('Something went wrong')).value).toBe( + 'Error: Something went wrong' + ) + }) + + it('should allow early termination with .return()', () => { + function* gen() { + yield 1 + yield 2 + yield 3 + } + + const g = gen() + + expect(g.next().value).toBe(1) + expect(g.return('early exit')).toEqual({ value: 'early exit', done: true }) + expect(g.next()).toEqual({ value: undefined, done: true }) + }) + }) +}) diff --git a/tests/functions-execution/iife-modules/iife-modules.test.js b/tests/functions-execution/iife-modules/iife-modules.test.js new file mode 100644 index 00000000..5fbdb147 --- /dev/null +++ b/tests/functions-execution/iife-modules/iife-modules.test.js @@ -0,0 +1,1250 @@ +import { describe, it, expect, vi } from 'vitest' + +describe('IIFE, Modules and Namespaces', () => { + // =========================================== + // Part 1: IIFE — The Self-Running Function + // =========================================== + + describe('Part 1: IIFE — The Self-Running Function', () => { + describe('What is an IIFE?', () => { + it('should require calling a normal function manually', () => { + let called = false + + function greet() { + called = true + } + + // Function is defined but not called yet + expect(called).toBe(false) + + greet() // You have to call it + expect(called).toBe(true) + }) + + it('should run IIFE immediately without manual call', () => { + let called = false + + // An IIFE — it runs immediately, no calling needed + ;(function () { + called = true + })() // Runs right away! + + expect(called).toBe(true) + }) + + it('should demonstrate IIFE executes during definition', () => { + const results = [] + + results.push('before IIFE') + ;(function () { + results.push('inside IIFE') + })() + results.push('after IIFE') + + expect(results).toEqual(['before IIFE', 'inside IIFE', 'after IIFE']) + }) + }) + + describe('IIFE Variations', () => { + it('should work with classic style', () => { + let executed = false + + // Classic style + ;(function () { + executed = true + })() + + expect(executed).toBe(true) + }) + + it('should work with alternative parentheses placement', () => { + let executed = false + + // Alternative parentheses placement + ;(function () { + executed = true + })() + + expect(executed).toBe(true) + }) + + it('should work with arrow function IIFE (modern)', () => { + let executed = false + + // Arrow function IIFE (modern) + ;(() => { + executed = true + })() + + expect(executed).toBe(true) + }) + + it('should work with parameters', () => { + let greeting = '' + + // With parameters + ;((name) => { + greeting = `Hello, ${name}!` + })('Alice') + + expect(greeting).toBe('Hello, Alice!') + }) + + it('should work with named IIFE (useful for debugging)', () => { + let executed = false + + // Named IIFE (useful for debugging) + ;(function myIIFE() { + executed = true + })() + + expect(executed).toBe(true) + }) + + it('should allow named IIFE to call itself recursively', () => { + const result = (function factorial(n) { + if (n <= 1) return 1 + return n * factorial(n - 1) + })(5) + + expect(result).toBe(120) + }) + }) + + describe('Why Were IIFEs Invented? (Global Scope Problem)', () => { + it('should demonstrate var variables can be overwritten in same scope', () => { + // Simulating file1.js + var userName = 'Alice' + var count = 0 + + // Simulating file2.js (loaded after file1.js) + var userName = 'Bob' // Overwrites the first userName + var count = 100 // Overwrites the first count + + // Now file1.js's code is broken because its variables were replaced + expect(userName).toBe('Bob') + expect(count).toBe(100) + }) + + it('should demonstrate IIFE creates private scope', () => { + let file1UserName + let file2UserName + + // file1.js — wrapped in an IIFE + ;(function () { + var userName = 'Alice' // Private to this IIFE + file1UserName = userName + })() + + // file2.js — also wrapped in an IIFE + ;(function () { + var userName = 'Bob' // Different variable, no conflict! + file2UserName = userName + })() + + expect(file1UserName).toBe('Alice') + expect(file2UserName).toBe('Bob') + }) + + it('should keep IIFE variables inaccessible from outside', () => { + ;(function () { + var privateVar = 'secret' + // privateVar exists here + expect(privateVar).toBe('secret') + })() + + // privateVar is not accessible here + expect(typeof privateVar).toBe('undefined') + }) + }) + + describe('Practical Example: Creating Private Variables (Module Pattern)', () => { + it('should create counter with private state', () => { + const counter = (function () { + // Private variable — can't be accessed directly + let count = 0 + + // Return public interface + return { + increment() { + count++ + }, + decrement() { + count-- + }, + getCount() { + return count + } + } + })() + + // Using the counter + counter.increment() + expect(counter.getCount()).toBe(1) + + counter.increment() + expect(counter.getCount()).toBe(2) + + counter.decrement() + expect(counter.getCount()).toBe(1) + }) + + it('should keep count private (not accessible directly)', () => { + const counter = (function () { + let count = 0 + + return { + increment() { + count++ + }, + getCount() { + return count + } + } + })() + + counter.increment() + counter.increment() + + // Trying to access private variables + expect(counter.count).toBe(undefined) // it's private! + }) + + it('should throw TypeError when calling non-existent private function', () => { + const counter = (function () { + let count = 0 + + // Private function — also hidden + function log(message) { + return `[Counter] ${message}` + } + + return { + increment() { + count++ + return log(`Incremented to ${count}`) + }, + getCount() { + return count + } + } + })() + + // Private log function works internally + expect(counter.increment()).toBe('[Counter] Incremented to 1') + + // But not accessible from outside + expect(counter.log).toBe(undefined) + expect(() => counter.log('test')).toThrow(TypeError) + }) + + it('should create multiple independent counter instances', () => { + function createCounter() { + let count = 0 + + return { + increment() { + count++ + }, + getCount() { + return count + } + } + } + + const counter1 = createCounter() + const counter2 = createCounter() + + counter1.increment() + counter1.increment() + counter1.increment() + + counter2.increment() + + expect(counter1.getCount()).toBe(3) + expect(counter2.getCount()).toBe(1) + }) + }) + + describe('IIFE with Parameters', () => { + it('should pass parameters into IIFE', () => { + const result = (function (a, b) { + return a + b + })(10, 20) + + expect(result).toBe(30) + }) + + it('should create local aliases for global objects', () => { + const globalObj = { name: 'Global' } + + const result = (function (obj) { + // Inside here, obj is a local reference + return obj.name + })(globalObj) + + expect(result).toBe('Global') + }) + + it('should preserve parameter values even if outer variable changes', () => { + let value = 'original' + + const getOriginalValue = (function (capturedValue) { + return function () { + return capturedValue + } + })(value) + + value = 'changed' + + expect(getOriginalValue()).toBe('original') + expect(value).toBe('changed') + }) + }) + + describe('When to Use IIFEs Today', () => { + describe('One-time initialization code', () => { + it('should create config object with computed values', () => { + const config = (() => { + const env = 'production' // simulating process.env.NODE_ENV + const apiUrl = + env === 'production' + ? 'https://api.example.com' + : 'http://localhost:3000' + + return { env, apiUrl } + })() + + expect(config.env).toBe('production') + expect(config.apiUrl).toBe('https://api.example.com') + }) + + it('should use development URL when not in production', () => { + const config = (() => { + const env = 'development' + const apiUrl = + env === 'production' + ? 'https://api.example.com' + : 'http://localhost:3000' + + return { env, apiUrl } + })() + + expect(config.env).toBe('development') + expect(config.apiUrl).toBe('http://localhost:3000') + }) + }) + + describe('Creating async IIFEs', () => { + it('should execute async IIFE', async () => { + let result = null + + await (async () => { + result = await Promise.resolve('data loaded') + })() + + expect(result).toBe('data loaded') + }) + + it('should handle async operations in IIFE', async () => { + const data = await (async () => { + const response = await Promise.resolve({ json: () => ({ id: 1, name: 'Test' }) }) + return response.json() + })() + + expect(data).toEqual({ id: 1, name: 'Test' }) + }) + }) + }) + }) + + // =========================================== + // Part 2: Namespaces — Organizing Under One Name + // =========================================== + + describe('Part 2: Namespaces — Organizing Under One Name', () => { + describe('What is a Namespace?', () => { + it('should demonstrate variables without namespace can conflict', () => { + // Without namespace — variables everywhere + var userName = 'Alice' + var userAge = 25 + + // These could easily conflict with other code + var userName = 'Bob' // Overwrites! + + expect(userName).toBe('Bob') + }) + + it('should organize data under one namespace object', () => { + // With namespace — everything organized under one name + const User = { + name: 'Alice', + age: 25, + email: 'alice@example.com', + + login() { + return `${this.name} logged in` + }, + logout() { + return `${this.name} logged out` + } + } + + // Access with the namespace prefix + expect(User.name).toBe('Alice') + expect(User.age).toBe(25) + expect(User.login()).toBe('Alice logged in') + expect(User.logout()).toBe('Alice logged out') + }) + }) + + describe('Creating a Namespace', () => { + it('should create simple namespace and add properties', () => { + // Simple namespace + const MyApp = {} + + // Add things to it + MyApp.version = '1.0.0' + MyApp.config = { + apiUrl: 'https://api.example.com', + timeout: 5000 + } + + expect(MyApp.version).toBe('1.0.0') + expect(MyApp.config.apiUrl).toBe('https://api.example.com') + expect(MyApp.config.timeout).toBe(5000) + }) + + it('should add utility methods to namespace', () => { + const MyApp = {} + + MyApp.utils = { + formatDate(date) { + return date.toLocaleDateString() + }, + capitalize(str) { + return str.charAt(0).toUpperCase() + str.slice(1) + } + } + + expect(MyApp.utils.capitalize('hello')).toBe('Hello') + expect(MyApp.utils.capitalize('world')).toBe('World') + + const testDate = new Date('2024-01-15') + expect(typeof MyApp.utils.formatDate(testDate)).toBe('string') + }) + }) + + describe('Nested Namespaces', () => { + it('should create nested namespace structure', () => { + // Create the main namespace + const MyApp = { + // Nested namespaces + Models: {}, + Views: {}, + Controllers: {}, + Utils: {} + } + + expect(MyApp.Models).toEqual({}) + expect(MyApp.Views).toEqual({}) + expect(MyApp.Controllers).toEqual({}) + expect(MyApp.Utils).toEqual({}) + }) + + it('should add functionality to nested namespaces', () => { + const MyApp = { + Models: {}, + Views: {}, + Utils: {} + } + + // Add to nested namespaces + MyApp.Models.User = { + create(name) { + return { name, id: Date.now() } + }, + find(id) { + return { id, name: 'Found User' } + } + } + + MyApp.Views.UserList = { + render(users) { + return users.map((u) => u.name).join(', ') + } + } + + MyApp.Utils.Validation = { + isEmail(str) { + return str.includes('@') + } + } + + // Use nested namespaces + const user = MyApp.Models.User.create('Alice') + expect(user.name).toBe('Alice') + expect(typeof user.id).toBe('number') + + const found = MyApp.Models.User.find(123) + expect(found.id).toBe(123) + + const rendered = MyApp.Views.UserList.render([{ name: 'Alice' }, { name: 'Bob' }]) + expect(rendered).toBe('Alice, Bob') + + expect(MyApp.Utils.Validation.isEmail('test@example.com')).toBe(true) + expect(MyApp.Utils.Validation.isEmail('invalid')).toBe(false) + }) + }) + + describe('Combining Namespaces with IIFEs', () => { + it('should create namespace with IIFE for private variables', () => { + const MyApp = {} + + // Use IIFE to add features with private variables + MyApp.Counter = (function () { + // Private + let count = 0 + + // Public + return { + increment() { + count++ + }, + decrement() { + count-- + }, + getCount() { + return count + } + } + })() + + MyApp.Counter.increment() + MyApp.Counter.increment() + expect(MyApp.Counter.getCount()).toBe(2) + + MyApp.Counter.decrement() + expect(MyApp.Counter.getCount()).toBe(1) + + // Private count is not accessible + expect(MyApp.Counter.count).toBe(undefined) + }) + + it('should create Logger with private logs array', () => { + const MyApp = {} + + MyApp.Logger = (function () { + // Private + const logs = [] + + // Public + return { + log(message) { + logs.push({ message, time: new Date() }) + return message // Return for testing + }, + getLogs() { + return [...logs] // Return a copy + }, + getLogCount() { + return logs.length + } + } + })() + + MyApp.Logger.log('First message') + MyApp.Logger.log('Second message') + + expect(MyApp.Logger.getLogCount()).toBe(2) + + const allLogs = MyApp.Logger.getLogs() + expect(allLogs[0].message).toBe('First message') + expect(allLogs[1].message).toBe('Second message') + + // Returned array is a copy, modifying it doesn't affect internal logs + allLogs.push({ message: 'Fake log' }) + expect(MyApp.Logger.getLogCount()).toBe(2) + }) + + it('should combine Counter and Logger in same namespace', () => { + const MyApp = {} + + MyApp.Counter = (function () { + let count = 0 + return { + increment() { + count++ + return count + }, + getCount() { + return count + } + } + })() + + MyApp.Logger = (function () { + const logs = [] + return { + log(message) { + logs.push(message) + }, + getLogs() { + return [...logs] + } + } + })() + + // Usage + const newCount = MyApp.Counter.increment() + MyApp.Logger.log(`Counter incremented to ${newCount}`) + + expect(MyApp.Counter.getCount()).toBe(1) + expect(MyApp.Logger.getLogs()).toEqual(['Counter incremented to 1']) + }) + }) + }) + + // =========================================== + // Part 3: ES6 Modules — The Modern Solution (Patterns) + // =========================================== + + describe('Part 3: ES6 Modules — Pattern Testing', () => { + describe('Named Export Patterns', () => { + it('should test function that would be named export', () => { + // These functions would be: export function add(a, b) { ... } + function add(a, b) { + return a + b + } + + function subtract(a, b) { + return a - b + } + + const PI = 3.14159 + + expect(add(2, 3)).toBe(5) + expect(subtract(10, 4)).toBe(6) + expect(PI).toBe(3.14159) + }) + + it('should test square and cube functions', () => { + // export function square(x) { return x * x; } + function square(x) { + return x * x + } + + // function cube(x) { return x * x * x; } + // export { cube }; + function cube(x) { + return x * x * x + } + + expect(square(4)).toBe(16) + expect(square(5)).toBe(25) + expect(cube(3)).toBe(27) + expect(cube(4)).toBe(64) + }) + + it('should test Calculator class', () => { + // export class Calculator { ... } + class Calculator { + add(a, b) { + return a + b + } + subtract(a, b) { + return a - b + } + multiply(a, b) { + return a * b + } + divide(a, b) { + return a / b + } + } + + const calc = new Calculator() + expect(calc.add(5, 3)).toBe(8) + expect(calc.subtract(10, 4)).toBe(6) + expect(calc.multiply(3, 4)).toBe(12) + expect(calc.divide(20, 5)).toBe(4) + }) + + it('should test math constants', () => { + const PI = 3.14159 + const E = 2.71828 + + expect(PI).toBeCloseTo(3.14159, 5) + expect(E).toBeCloseTo(2.71828, 5) + }) + }) + + describe('Default Export Patterns', () => { + it('should test greet function (default export pattern)', () => { + // export default function greet(name) { ... } + function greet(name) { + return `Hello, ${name}!` + } + + expect(greet('World')).toBe('Hello, World!') + expect(greet('Alice')).toBe('Hello, Alice!') + }) + + it('should test User class (default export pattern)', () => { + // export default class User { ... } + class User { + constructor(name) { + this.name = name + } + + greet() { + return `Hi, I'm ${this.name}` + } + } + + const user = new User('Alice') + expect(user.name).toBe('Alice') + expect(user.greet()).toBe("Hi, I'm Alice") + + const bob = new User('Bob') + expect(bob.greet()).toBe("Hi, I'm Bob") + }) + }) + + describe('Module Privacy Pattern', () => { + it('should demonstrate unexported variables are private', () => { + // In a real module: + // let currentUser = null; // Private to this module + // export function login() { currentUser = ... } + // export function getCurrentUser() { return currentUser } + + // Simulating with closure + const authModule = (function () { + let currentUser = null // Private to this module + + return { + login(email) { + currentUser = { email, loggedInAt: new Date() } + return currentUser + }, + getCurrentUser() { + return currentUser + }, + logout() { + currentUser = null + } + } + })() + + expect(authModule.getCurrentUser()).toBe(null) + + authModule.login('user@example.com') + expect(authModule.getCurrentUser().email).toBe('user@example.com') + + authModule.logout() + expect(authModule.getCurrentUser()).toBe(null) + + // currentUser is not directly accessible + expect(authModule.currentUser).toBe(undefined) + }) + }) + + describe('Re-export Pattern (Barrel Files)', () => { + it('should demonstrate re-export concept', () => { + // utils/format.js + const formatModule = { + formatDate(date) { + return date.toLocaleDateString() + }, + formatCurrency(amount) { + return `$${amount.toFixed(2)}` + } + } + + // utils/validate.js + const validateModule = { + isEmail(str) { + return str.includes('@') + }, + isPhone(str) { + return /^\d{10}$/.test(str) + } + } + + // utils/index.js — re-exports everything + const Utils = { + ...formatModule, + ...validateModule + } + + expect(Utils.formatCurrency(19.99)).toBe('$19.99') + expect(Utils.isEmail('test@example.com')).toBe(true) + expect(Utils.isPhone('1234567890')).toBe(true) + expect(Utils.isPhone('123')).toBe(false) + }) + }) + }) + + // =========================================== + // The Evolution: From IIFEs to Modules + // =========================================== + + describe('The Evolution: From IIFEs to Modules', () => { + describe('Era 1: Global (Bad)', () => { + it('should demonstrate global variable problem', () => { + // Everything pollutes global scope + var counter = 0 + + function increment() { + counter++ + } + + function getCount() { + return counter + } + + increment() + expect(getCount()).toBe(1) + + // Problem: Anyone can do this + counter = 999 // Oops, state corrupted! + expect(getCount()).toBe(999) + }) + }) + + describe('Era 2: IIFE (Better)', () => { + it('should demonstrate IIFE protects state', () => { + // Uses closure to hide counter + var Counter = (function () { + var counter = 0 // Private! + + return { + increment: function () { + counter++ + }, + getCount: function () { + return counter + } + } + })() + + Counter.increment() + expect(Counter.getCount()).toBe(1) + expect(Counter.counter).toBe(undefined) // private! + }) + + it('should prevent external modification of private state', () => { + var Counter = (function () { + var counter = 0 + + return { + increment: function () { + counter++ + }, + getCount: function () { + return counter + } + } + })() + + Counter.increment() + Counter.increment() + Counter.increment() + + // Cannot directly set counter + Counter.counter = 999 + expect(Counter.getCount()).toBe(3) // Still 3, not 999! + }) + }) + + describe('Era 3: ES6 Modules (Best) - Pattern', () => { + it('should demonstrate module pattern (simulated)', () => { + // Simulating: + // counter.js + // let counter = 0; // Private (not exported) + // export function increment() { counter++; } + // export function getCount() { return counter; } + + const counterModule = (function () { + let counter = 0 // Private (not exported) + + return { + increment() { + counter++ + }, + getCount() { + return counter + } + } + })() + + // Simulating: import { increment, getCount } from './counter.js'; + const { increment, getCount } = counterModule + + increment() + expect(getCount()).toBe(1) + + // counter variable is not accessible at all + expect(counterModule.counter).toBe(undefined) + }) + }) + }) + + // =========================================== + // Common Patterns and Best Practices + // =========================================== + + describe('Common Patterns and Best Practices', () => { + describe('Avoid Circular Dependencies', () => { + it('should demonstrate shared module pattern to avoid circular deps', () => { + // Instead of A importing B and B importing A... + // Create a third module for shared code + + // shared.js + const sharedModule = { + sharedThing: 'shared', + helperFunction() { + return 'helper result' + } + } + + // a.js - imports from shared + const moduleA = { + fromA: 'A', + useShared() { + return sharedModule.sharedThing + } + } + + // b.js - also imports from shared + const moduleB = { + fromB: 'B', + useShared() { + return sharedModule.sharedThing + } + } + + // No circular dependency! + expect(moduleA.useShared()).toBe('shared') + expect(moduleB.useShared()).toBe('shared') + }) + }) + }) + + // =========================================== + // Test Your Knowledge Examples + // =========================================== + + describe('Test Your Knowledge Examples', () => { + describe('Question 1: What does IIFE stand for?', () => { + it('should demonstrate Immediately Invoked Function Expression', () => { + const results = [] + + // Immediately - runs right now + // Invoked - called/executed + // Function Expression - a function written as an expression + + results.push('before') + ;(function () { + results.push('immediately invoked') + })() + results.push('after') + + expect(results).toEqual(['before', 'immediately invoked', 'after']) + }) + }) + + describe('Question 3: How to create private variable in IIFE?', () => { + it('should create private variable inside IIFE', () => { + const module = (function () { + // Private variable + let privateCounter = 0 + + // Return public methods that can access it + return { + increment() { + privateCounter++ + }, + getCount() { + return privateCounter + } + } + })() + + module.increment() + expect(module.getCount()).toBe(1) + expect(module.privateCounter).toBe(undefined) // private! + }) + }) + + describe('Question 4: Static vs Dynamic imports', () => { + it('should demonstrate dynamic import returns a Promise', async () => { + // Dynamic imports return Promises + // const module = await import('./module.js') + + // Simulating dynamic import behavior + const dynamicImport = () => + Promise.resolve({ + default: function () { + return 'loaded' + }, + namedExport: 'value' + }) + + const module = await dynamicImport() + expect(module.default()).toBe('loaded') + expect(module.namedExport).toBe('value') + }) + }) + + describe('Question 6: When to use IIFE today', () => { + it('should use IIFE for async initialization', async () => { + let data = null + + await (async () => { + data = await Promise.resolve({ loaded: true }) + })() + + expect(data).toEqual({ loaded: true }) + }) + + it('should use IIFE for one-time calculations', () => { + const config = (() => { + // Complex setup that runs once + const computed = 2 + 2 + return { computed, ready: true } + })() + + expect(config.computed).toBe(4) + expect(config.ready).toBe(true) + }) + }) + }) + + // =========================================== + // Additional Edge Cases + // =========================================== + + describe('Edge Cases and Additional Tests', () => { + describe('IIFE Return Values', () => { + it('should return primitive values from IIFE', () => { + const number = (function () { + return 42 + })() + + const string = (function () { + return 'hello' + })() + + const boolean = (function () { + return true + })() + + expect(number).toBe(42) + expect(string).toBe('hello') + expect(boolean).toBe(true) + }) + + it('should return undefined when no return statement', () => { + const result = (function () { + const x = 1 + 1 // no return + })() + + expect(result).toBe(undefined) + }) + + it('should return arrays and objects from IIFE', () => { + const arr = (function () { + return [1, 2, 3] + })() + + const obj = (function () { + return { a: 1, b: 2 } + })() + + expect(arr).toEqual([1, 2, 3]) + expect(obj).toEqual({ a: 1, b: 2 }) + }) + }) + + describe('Nested IIFEs', () => { + it('should support nested IIFEs', () => { + const result = (function () { + const outer = 'outer' + + return (function () { + const inner = 'inner' + return outer + '-' + inner + })() + })() + + expect(result).toBe('outer-inner') + }) + }) + + describe('IIFE with this context', () => { + it('should demonstrate arrow IIFE inherits this from enclosing scope', () => { + // Arrow functions don't have their own this binding + // They inherit this from the enclosing lexical scope + const obj = { + name: 'TestObject', + getThisArrow: (() => { + // In module scope, this may be undefined or global depending on environment + return typeof this + })(), + getNameWithRegular: function() { + return (function() { + // Regular function IIFE in strict mode has undefined this + return this + })() + } + } + + // Arrow IIFE inherits this from module scope (not the object) + expect(typeof obj.getThisArrow).toBe('string') + + // Regular function IIFE called without context has undefined this in strict mode + expect(obj.getNameWithRegular()).toBe(undefined) + }) + + it('should show regular function IIFE has undefined this in strict mode', () => { + const result = (function() { + 'use strict' + return this + })() + + expect(result).toBe(undefined) + }) + }) + + describe('Namespace Extension', () => { + it('should extend existing namespace safely', () => { + // Create or extend namespace + const MyApp = {} + + // Safely extend (pattern used in large apps) + MyApp.Utils = MyApp.Utils || {} + MyApp.Utils.String = MyApp.Utils.String || {} + + MyApp.Utils.String.reverse = function (str) { + return str.split('').reverse().join('') + } + + expect(MyApp.Utils.String.reverse('hello')).toBe('olleh') + + // Extend again without overwriting + MyApp.Utils.String = MyApp.Utils.String || {} + MyApp.Utils.String.uppercase = function (str) { + return str.toUpperCase() + } + + // Both functions exist + expect(MyApp.Utils.String.reverse('test')).toBe('tset') + expect(MyApp.Utils.String.uppercase('test')).toBe('TEST') + }) + }) + + describe('Closure over Loop Variables', () => { + it('should demonstrate IIFE fixing var loop problem', () => { + const funcs = [] + + for (var i = 0; i < 3; i++) { + ;(function (j) { + funcs.push(function () { + return j + }) + })(i) + } + + expect(funcs[0]()).toBe(0) + expect(funcs[1]()).toBe(1) + expect(funcs[2]()).toBe(2) + }) + + it('should show problem without IIFE', () => { + const funcs = [] + + for (var i = 0; i < 3; i++) { + funcs.push(function () { + return i + }) + } + + // All return 3 because they share the same i + expect(funcs[0]()).toBe(3) + expect(funcs[1]()).toBe(3) + expect(funcs[2]()).toBe(3) + }) + }) + + describe('Module Pattern Variations', () => { + it('should implement revealing module pattern', () => { + const RevealingModule = (function () { + // Private variables and functions + let privateVar = 'private' + let publicVar = 'public' + + function privateFunction() { + return privateVar + } + + function publicFunction() { + return publicVar + } + + function setPrivate(val) { + privateVar = val + } + + // Reveal public pointers to private functions + return { + publicVar, + publicFunction, + setPrivate, + getPrivate: privateFunction + } + })() + + expect(RevealingModule.publicVar).toBe('public') + expect(RevealingModule.publicFunction()).toBe('public') + expect(RevealingModule.getPrivate()).toBe('private') + + RevealingModule.setPrivate('updated') + expect(RevealingModule.getPrivate()).toBe('updated') + + // Private function not accessible directly + expect(RevealingModule.privateFunction).toBe(undefined) + }) + + it('should implement singleton pattern with IIFE', () => { + const Singleton = (function () { + let instance + + function createInstance() { + return { + id: Math.random(), + getName() { + return 'Singleton Instance' + } + } + } + + return { + getInstance() { + if (!instance) { + instance = createInstance() + } + return instance + } + } + })() + + const instance1 = Singleton.getInstance() + const instance2 = Singleton.getInstance() + + expect(instance1).toBe(instance2) // Same instance + expect(instance1.id).toBe(instance2.id) + }) + }) + }) +}) diff --git a/tests/functions-execution/promises/promises.test.js b/tests/functions-execution/promises/promises.test.js new file mode 100644 index 00000000..1ca0ff2f --- /dev/null +++ b/tests/functions-execution/promises/promises.test.js @@ -0,0 +1,529 @@ +import { describe, it, expect, vi } from 'vitest' + +describe('Promises', () => { + describe('Basic Promise Creation', () => { + it('should create a fulfilled Promise with resolve()', async () => { + const promise = new Promise((resolve) => { + resolve('success') + }) + + const result = await promise + expect(result).toBe('success') + }) + + it('should create a rejected Promise with reject()', async () => { + const promise = new Promise((_, reject) => { + reject(new Error('failure')) + }) + + await expect(promise).rejects.toThrow('failure') + }) + + it('should execute the executor function synchronously', () => { + const order = [] + + order.push('before') + + new Promise((resolve) => { + order.push('inside executor') + resolve('done') + }) + + order.push('after') + + expect(order).toEqual(['before', 'inside executor', 'after']) + }) + + it('should ignore subsequent resolve/reject calls after first settlement', async () => { + const promise = new Promise((resolve, reject) => { + resolve('first') + resolve('second') // Ignored + reject(new Error('error')) // Ignored + }) + + const result = await promise + expect(result).toBe('first') + }) + + it('should automatically reject if executor throws', async () => { + const promise = new Promise(() => { + throw new Error('thrown error') + }) + + await expect(promise).rejects.toThrow('thrown error') + }) + }) + + describe('Promise.resolve() and Promise.reject()', () => { + it('should create fulfilled Promise with Promise.resolve()', async () => { + const promise = Promise.resolve(42) + expect(await promise).toBe(42) + }) + + it('should create rejected Promise with Promise.reject()', async () => { + const promise = Promise.reject(new Error('rejected')) + await expect(promise).rejects.toThrow('rejected') + }) + + it('should return the same Promise if resolving with a Promise', async () => { + const original = Promise.resolve('original') + const wrapped = Promise.resolve(original) + + // Promise.resolve returns the same Promise if given a native Promise + expect(wrapped).toBe(original) + }) + }) + + describe('.then() method', () => { + it('should receive the fulfilled value', async () => { + const result = await Promise.resolve(10).then(x => x * 2) + expect(result).toBe(20) + }) + + it('should return a new Promise', () => { + const p1 = Promise.resolve(1) + const p2 = p1.then(x => x) + + expect(p2).toBeInstanceOf(Promise) + expect(p1).not.toBe(p2) + }) + + it('should chain values through multiple .then() calls', async () => { + const result = await Promise.resolve(1) + .then(x => x + 1) + .then(x => x * 2) + .then(x => x + 10) + + expect(result).toBe(14) // ((1 + 1) * 2) + 10 + }) + + it('should unwrap returned Promises', async () => { + const result = await Promise.resolve(1) + .then(x => Promise.resolve(x + 1)) + .then(x => x * 2) + + expect(result).toBe(4) // (1 + 1) * 2 + }) + + it('should skip .then() when Promise is rejected', async () => { + const thenCallback = vi.fn() + + await Promise.reject(new Error('error')) + .then(thenCallback) + .catch(() => {}) // Handle the rejection + + expect(thenCallback).not.toHaveBeenCalled() + }) + }) + + describe('.catch() method', () => { + it('should catch rejected Promises', async () => { + const result = await Promise.reject(new Error('error')) + .catch(error => `caught: ${error.message}`) + + expect(result).toBe('caught: error') + }) + + it('should catch errors thrown in .then()', async () => { + const result = await Promise.resolve('ok') + .then(() => { + throw new Error('thrown') + }) + .catch(error => `caught: ${error.message}`) + + expect(result).toBe('caught: thrown') + }) + + it('should allow chain to continue after catching', async () => { + const result = await Promise.reject(new Error('error')) + .catch(() => 'recovered') + .then(value => value.toUpperCase()) + + expect(result).toBe('RECOVERED') + }) + + it('should propagate errors through the chain until caught', async () => { + const thenCallback1 = vi.fn() + const thenCallback2 = vi.fn() + const catchCallback = vi.fn(e => e.message) + + await Promise.reject(new Error('original error')) + .then(thenCallback1) + .then(thenCallback2) + .catch(catchCallback) + + expect(thenCallback1).not.toHaveBeenCalled() + expect(thenCallback2).not.toHaveBeenCalled() + expect(catchCallback).toHaveBeenCalledWith(expect.any(Error)) + }) + }) + + describe('.finally() method', () => { + it('should run on fulfillment', async () => { + const finallyCallback = vi.fn() + + await Promise.resolve('value').finally(finallyCallback) + + expect(finallyCallback).toHaveBeenCalled() + }) + + it('should run on rejection', async () => { + const finallyCallback = vi.fn() + + await Promise.reject(new Error('error')) + .catch(() => {}) // Handle rejection + .finally(finallyCallback) + + expect(finallyCallback).toHaveBeenCalled() + }) + + it('should not receive any arguments', async () => { + const finallyCallback = vi.fn() + + await Promise.resolve('value').finally(finallyCallback) + + expect(finallyCallback).toHaveBeenCalledWith() // No arguments + }) + + it('should pass through the original value', async () => { + const result = await Promise.resolve('original') + .finally(() => 'ignored') + + expect(result).toBe('original') + }) + + it('should pass through the original error', async () => { + await expect( + Promise.reject(new Error('original')) + .finally(() => 'ignored') + ).rejects.toThrow('original') + }) + }) + + describe('Promise Chaining', () => { + it('should maintain chain with undefined return', async () => { + const result = await Promise.resolve('start') + .then(() => { + // No explicit return = undefined + }) + .then(value => value) + + expect(result).toBeUndefined() + }) + + it('should handle async operations in sequence', async () => { + const delay = (ms, value) => + new Promise(resolve => setTimeout(() => resolve(value), ms)) + + const result = await delay(10, 'first') + .then(value => delay(10, value + ' second')) + .then(value => delay(10, value + ' third')) + + expect(result).toBe('first second third') + }) + }) + + describe('Promise.all()', () => { + it('should resolve with array of values when all fulfill', async () => { + const result = await Promise.all([ + Promise.resolve(1), + Promise.resolve(2), + Promise.resolve(3) + ]) + + expect(result).toEqual([1, 2, 3]) + }) + + it('should maintain order regardless of resolution order', async () => { + const result = await Promise.all([ + new Promise(resolve => setTimeout(() => resolve('slow'), 30)), + new Promise(resolve => setTimeout(() => resolve('fast'), 10)), + Promise.resolve('instant') + ]) + + expect(result).toEqual(['slow', 'fast', 'instant']) + }) + + it('should reject immediately if any Promise rejects', async () => { + await expect( + Promise.all([ + Promise.resolve('A'), + Promise.reject(new Error('B failed')), + Promise.resolve('C') + ]) + ).rejects.toThrow('B failed') + }) + + it('should work with non-Promise values', async () => { + const result = await Promise.all([1, 'two', Promise.resolve(3)]) + expect(result).toEqual([1, 'two', 3]) + }) + + it('should resolve immediately with empty array', async () => { + const result = await Promise.all([]) + expect(result).toEqual([]) + }) + }) + + describe('Promise.allSettled()', () => { + it('should return status objects for all Promises', async () => { + const results = await Promise.allSettled([ + Promise.resolve('success'), + Promise.reject(new Error('failure')), + Promise.resolve(42) + ]) + + expect(results).toEqual([ + { status: 'fulfilled', value: 'success' }, + { status: 'rejected', reason: expect.any(Error) }, + { status: 'fulfilled', value: 42 } + ]) + }) + + it('should never reject', async () => { + const results = await Promise.allSettled([ + Promise.reject(new Error('error 1')), + Promise.reject(new Error('error 2')) + ]) + + expect(results).toHaveLength(2) + expect(results[0].status).toBe('rejected') + expect(results[1].status).toBe('rejected') + }) + + it('should wait for all to settle', async () => { + const start = Date.now() + + await Promise.allSettled([ + new Promise(resolve => setTimeout(resolve, 50)), + new Promise((_, reject) => setTimeout(() => reject(new Error()), 30)), + new Promise(resolve => setTimeout(resolve, 40)) + ]) + + const elapsed = Date.now() - start + expect(elapsed).toBeGreaterThanOrEqual(45) // Waited for slowest + }) + }) + + describe('Promise.race()', () => { + it('should resolve with first settled value', async () => { + const result = await Promise.race([ + new Promise(resolve => setTimeout(() => resolve('slow'), 50)), + new Promise(resolve => setTimeout(() => resolve('fast'), 10)) + ]) + + expect(result).toBe('fast') + }) + + it('should reject if first settled is rejection', async () => { + await expect( + Promise.race([ + new Promise((_, reject) => setTimeout(() => reject(new Error('fast error')), 10)), + new Promise(resolve => setTimeout(() => resolve('slow success'), 50)) + ]) + ).rejects.toThrow('fast error') + }) + + it('should never settle with empty array', () => { + // Promise.race([]) returns a forever-pending Promise + const promise = Promise.race([]) + + // We can't really test this without timing out, + // but we can verify it returns a Promise + expect(promise).toBeInstanceOf(Promise) + }) + }) + + describe('Promise.any()', () => { + it('should resolve with first fulfilled value', async () => { + const result = await Promise.any([ + Promise.reject(new Error('error 1')), + Promise.resolve('success'), + Promise.reject(new Error('error 2')) + ]) + + expect(result).toBe('success') + }) + + it('should wait for first fulfillment, ignoring rejections', async () => { + const result = await Promise.any([ + new Promise((_, reject) => setTimeout(() => reject(new Error()), 10)), + new Promise(resolve => setTimeout(() => resolve('winner'), 30)), + new Promise((_, reject) => setTimeout(() => reject(new Error()), 20)) + ]) + + expect(result).toBe('winner') + }) + + it('should reject with AggregateError if all reject', async () => { + try { + await Promise.any([ + Promise.reject(new Error('error 1')), + Promise.reject(new Error('error 2')), + Promise.reject(new Error('error 3')) + ]) + expect.fail('Should have rejected') + } catch (error) { + expect(error.name).toBe('AggregateError') + expect(error.errors).toHaveLength(3) + } + }) + }) + + describe('Microtask Queue Timing', () => { + it('should run .then() callbacks asynchronously', () => { + const order = [] + + order.push('1') + + Promise.resolve().then(() => { + order.push('3') + }) + + order.push('2') + + // Synchronously, only 1 and 2 are in the array + expect(order).toEqual(['1', '2']) + }) + + it('should demonstrate microtask priority over macrotasks', async () => { + const order = [] + + // Macrotask (setTimeout) + setTimeout(() => order.push('timeout'), 0) + + // Microtask (Promise) + Promise.resolve().then(() => order.push('promise')) + + // Wait for both to complete + await new Promise(resolve => setTimeout(resolve, 10)) + + // Promise (microtask) runs before setTimeout (macrotask) + expect(order).toEqual(['promise', 'timeout']) + }) + + it('should process nested microtasks before macrotasks', async () => { + const order = [] + + setTimeout(() => order.push('timeout'), 0) + + Promise.resolve().then(() => { + order.push('promise 1') + Promise.resolve().then(() => { + order.push('promise 2') + }) + }) + + await new Promise(resolve => setTimeout(resolve, 10)) + + expect(order).toEqual(['promise 1', 'promise 2', 'timeout']) + }) + }) + + describe('Common Patterns', () => { + it('should wrap setTimeout in a Promise (delay pattern)', async () => { + const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms)) + + const start = Date.now() + await delay(50) + const elapsed = Date.now() - start + + expect(elapsed).toBeGreaterThanOrEqual(45) + }) + + it('should handle sequential execution', async () => { + const results = [] + const items = [1, 2, 3] + + for (const item of items) { + const result = await Promise.resolve(item * 2) + results.push(result) + } + + expect(results).toEqual([2, 4, 6]) + }) + + it('should handle parallel execution', async () => { + const items = [1, 2, 3] + const results = await Promise.all( + items.map(item => Promise.resolve(item * 2)) + ) + + expect(results).toEqual([2, 4, 6]) + }) + }) + + describe('Common Mistakes', () => { + it('should demonstrate forgotten return issue', async () => { + // This is what happens when you forget to return + const result = await Promise.resolve('start') + .then(value => { + Promise.resolve(value + ' middle') // Forgot return! + }) + .then(value => value) + + expect(result).toBeUndefined() // Lost the value! + }) + + it('should demonstrate correct return', async () => { + const result = await Promise.resolve('start') + .then(value => { + return Promise.resolve(value + ' middle') // Correct! + }) + .then(value => value) + + expect(result).toBe('start middle') + }) + + it('should demonstrate Promise constructor anti-pattern', async () => { + // Anti-pattern: unnecessary wrapper + const antiPattern = () => { + return new Promise((resolve, reject) => { + Promise.resolve('data') + .then(data => resolve(data)) + .catch(error => reject(error)) + }) + } + + // Correct: just return the Promise + const correct = () => { + return Promise.resolve('data') + } + + // Both work, but correct is cleaner + expect(await antiPattern()).toBe('data') + expect(await correct()).toBe('data') + }) + }) + + describe('Error Handling Patterns', () => { + it('should catch errors anywhere in the chain', async () => { + const error = await Promise.resolve('start') + .then(() => { + throw new Error('middle error') + }) + .then(() => 'never reached') + .catch(e => e.message) + + expect(error).toBe('middle error') + }) + + it('should allow recovery from errors', async () => { + const result = await Promise.reject(new Error('initial error')) + .catch(() => 'recovered value') + .then(value => value.toUpperCase()) + + expect(result).toBe('RECOVERED VALUE') + }) + + it('should allow re-throwing errors', async () => { + await expect( + Promise.reject(new Error('original')) + .catch(error => { + // Log it, then re-throw + throw error + }) + ).rejects.toThrow('original') + }) + }) +}) diff --git a/tests/fundamentals/call-stack/call-stack.test.js b/tests/fundamentals/call-stack/call-stack.test.js new file mode 100644 index 00000000..615ffbc4 --- /dev/null +++ b/tests/fundamentals/call-stack/call-stack.test.js @@ -0,0 +1,280 @@ +import { describe, it, expect } from 'vitest' + +describe('Call Stack', () => { + describe('Basic Function Calls', () => { + it('should execute nested function calls and return correct greeting', () => { + function createGreeting(name) { + return "Hello, " + name + "!" + } + + function greet(name) { + const greeting = createGreeting(name) + return greeting + } + + expect(greet("Alice")).toBe("Hello, Alice!") + }) + + it('should demonstrate function arguments in execution context', () => { + function greet(name, age) { + return { name, age } + } + + const result = greet("Alice", 25) + expect(result).toEqual({ name: "Alice", age: 25 }) + }) + + it('should demonstrate local variables in execution context', () => { + function calculate() { + const x = 10 + let y = 20 + var z = 30 + return x + y + z + } + + expect(calculate()).toBe(60) + }) + }) + + describe('Nested Function Calls', () => { + it('should execute multiply, square, and printSquare correctly', () => { + function multiply(x, y) { + return x * y + } + + function square(n) { + return multiply(n, n) + } + + function printSquare(n) { + const result = square(n) + return result + } + + expect(printSquare(4)).toBe(16) + }) + + it('should handle deep nesting of function calls', () => { + function a() { return b() } + function b() { return c() } + function c() { return d() } + function d() { return 'done' } + + expect(a()).toBe('done') + }) + + it('should calculate maximum stack depth correctly for nested calls', () => { + // This test verifies the example where max depth is 4 + let maxDepth = 0 + let currentDepth = 0 + + function a() { + currentDepth++ + maxDepth = Math.max(maxDepth, currentDepth) + const result = b() + currentDepth-- + return result + } + function b() { + currentDepth++ + maxDepth = Math.max(maxDepth, currentDepth) + const result = c() + currentDepth-- + return result + } + function c() { + currentDepth++ + maxDepth = Math.max(maxDepth, currentDepth) + const result = d() + currentDepth-- + return result + } + function d() { + currentDepth++ + maxDepth = Math.max(maxDepth, currentDepth) + currentDepth-- + return 'done' + } + + a() + expect(maxDepth).toBe(4) + }) + }) + + describe('Scope Chain in Execution Context', () => { + it('should access outer scope variables from inner function', () => { + function outer() { + const message = "Hello" + + function inner() { + return message + } + + return inner() + } + + expect(outer()).toBe("Hello") + }) + + it('should demonstrate this keyword context in objects', () => { + const person = { + name: "Alice", + greet() { + return this.name + } + } + + expect(person.greet()).toBe("Alice") + }) + }) + + describe('Stack Overflow', () => { + it('should throw RangeError for infinite recursion without base case', () => { + function countdown(n) { + countdown(n - 1) // No base case - infinite recursion + } + + expect(() => countdown(5)).toThrow(RangeError) + }) + + it('should work correctly with proper base case', () => { + const results = [] + + function countdown(n) { + if (n <= 0) { + results.push("Done!") + return + } + results.push(n) + countdown(n - 1) + } + + countdown(5) + expect(results).toEqual([5, 4, 3, 2, 1, "Done!"]) + }) + + it('should throw for infinite loop function', () => { + function loop() { + loop() + } + + expect(() => loop()).toThrow(RangeError) + }) + + it('should throw for base case that is never reached', () => { + function countUp(n, limit = 100) { + // Modified to have a safety limit for testing + if (n >= 1000000000000 || limit <= 0) return n + return countUp(n + 1, limit - 1) + } + + // This will return before hitting the impossible base case + expect(countUp(0)).toBe(100) + }) + + it('should throw for circular function calls', () => { + function a() { return b() } + function b() { return a() } + + expect(() => a()).toThrow(RangeError) + }) + + it('should throw for accidental recursion in setters', () => { + class Person { + set name(value) { + this.name = value // Calls the setter again - infinite loop! + } + } + + const p = new Person() + expect(() => { p.name = "Alice" }).toThrow(RangeError) + }) + + it('should work correctly with proper setter implementation using different property', () => { + class PersonFixed { + set name(value) { + this._name = value // Use _name instead to avoid recursion + } + get name() { + return this._name + } + } + + const p = new PersonFixed() + p.name = "Alice" + expect(p.name).toBe("Alice") + }) + }) + + describe('Recursion with Base Case', () => { + it('should calculate factorial correctly', () => { + function factorial(n) { + if (n <= 1) return 1 + return n * factorial(n - 1) + } + + expect(factorial(5)).toBe(120) + expect(factorial(1)).toBe(1) + expect(factorial(0)).toBe(1) + }) + + it('should demonstrate proper countdown with base case', () => { + function countdown(n) { + if (n <= 0) { + return "Done!" + } + return countdown(n - 1) + } + + expect(countdown(5)).toBe("Done!") + }) + }) + + describe('Error Stack Traces', () => { + it('should create error with proper stack trace', () => { + function a() { return b() } + function b() { return c() } + function c() { + throw new Error('Something went wrong!') + } + + expect(() => a()).toThrow('Something went wrong!') + }) + + it('should preserve call stack in error', () => { + function a() { return b() } + function b() { return c() } + function c() { + throw new Error('Test error') + } + + try { + a() + } catch (error) { + expect(error.stack).toContain('c') + expect(error.stack).toContain('b') + expect(error.stack).toContain('a') + } + }) + }) + + describe('Asynchronous Code Preview', () => { + it('should demonstrate setTimeout behavior with call stack', async () => { + const results = [] + + results.push('First') + + await new Promise(resolve => { + setTimeout(() => { + results.push('Second') + resolve() + }, 0) + + results.push('Third') + }) + + // Even with 0ms delay, 'Third' runs before 'Second' + expect(results).toEqual(['First', 'Third', 'Second']) + }) + }) +}) diff --git a/tests/fundamentals/equality-operators/equality-operators.test.js b/tests/fundamentals/equality-operators/equality-operators.test.js new file mode 100644 index 00000000..652586db --- /dev/null +++ b/tests/fundamentals/equality-operators/equality-operators.test.js @@ -0,0 +1,778 @@ +import { describe, it, expect } from 'vitest' + +describe('Equality and Type Checking', () => { + describe('Three Equality Operators Overview', () => { + it('should demonstrate different results for same comparison', () => { + const num = 1 + const str = "1" + + expect(num == str).toBe(true) // coerces string to number + expect(num === str).toBe(false) // different types + expect(Object.is(num, str)).toBe(false) // different types + }) + }) + + describe('Loose Equality (==)', () => { + describe('Same Type Comparison', () => { + it('should compare directly when same type', () => { + expect(5 == 5).toBe(true) + expect("hello" == "hello").toBe(true) + }) + }) + + describe('null and undefined', () => { + it('should return true for null == undefined', () => { + expect(null == undefined).toBe(true) + expect(undefined == null).toBe(true) + }) + }) + + describe('Number and String', () => { + it('should convert string to number', () => { + expect(5 == "5").toBe(true) + expect(0 == "").toBe(true) + expect(100 == "1e2").toBe(true) + }) + + it('should return false for different string comparison', () => { + expect("" == "0").toBe(false) // Both strings, different values + }) + + it('should handle NaN conversions', () => { + expect(NaN == "NaN").toBe(false) // NaN ≠ anything + expect(0 == "hello").toBe(false) // "hello" → NaN + }) + }) + + describe('BigInt and String', () => { + it('should convert string to BigInt', () => { + expect(10n == "10").toBe(true) + }) + }) + + describe('Boolean Coercion', () => { + it('should convert boolean to number first', () => { + expect(true == 1).toBe(true) + expect(false == 0).toBe(true) + expect(true == "1").toBe(true) + expect(false == "").toBe(true) + }) + + it('should demonstrate confusing boolean comparisons', () => { + expect(true == "true").toBe(false) // true → 1, "true" → NaN + expect(false == "false").toBe(false) // false → 0, "false" → NaN + expect(true == 2).toBe(false) // true → 1, 1 ≠ 2 + expect(true == "2").toBe(false) // true → 1, "2" → 2 + }) + }) + + describe('Object to Primitive', () => { + it('should convert object via ToPrimitive', () => { + expect([1] == 1).toBe(true) // [1] → "1" → 1 + expect([""] == 0).toBe(true) // [""] → "" → 0 + }) + }) + + describe('BigInt and Number', () => { + it('should compare mathematical values', () => { + expect(10n == 10).toBe(true) + expect(10n == 10.5).toBe(false) + }) + }) + + describe('Special Cases', () => { + it('should return false for null/undefined vs other values', () => { + expect(null == 0).toBe(false) + expect(undefined == 0).toBe(false) + expect(Symbol() == Symbol()).toBe(false) + }) + }) + + describe('Surprising Results', () => { + describe('String and Number', () => { + it('should demonstrate string to number conversions', () => { + expect(1 == "1").toBe(true) + expect(0 == "").toBe(true) + expect(0 == "0").toBe(true) + expect(100 == "1e2").toBe(true) + }) + }) + + describe('null and undefined', () => { + it('should demonstrate special null/undefined behavior', () => { + expect(null == undefined).toBe(true) + expect(null == 0).toBe(false) + expect(null == false).toBe(false) + expect(null == "").toBe(false) + expect(undefined == 0).toBe(false) + expect(undefined == false).toBe(false) + }) + + it('should catch both null and undefined with == null', () => { + function greet(name) { + if (name == null) { + return "Hello, stranger!" + } + return `Hello, ${name}!` + } + + expect(greet(null)).toBe("Hello, stranger!") + expect(greet(undefined)).toBe("Hello, stranger!") + expect(greet("Alice")).toBe("Hello, Alice!") + expect(greet("")).toBe("Hello, !") + expect(greet(0)).toBe("Hello, 0!") + }) + }) + + describe('Arrays and Objects', () => { + it('should convert arrays via ToPrimitive', () => { + expect([] == false).toBe(true) + expect([] == 0).toBe(true) + expect([] == "").toBe(true) + expect([1] == 1).toBe(true) + expect([1] == "1").toBe(true) + expect([1, 2] == "1,2").toBe(true) + }) + + it('should use valueOf on objects with custom valueOf', () => { + let obj = { valueOf: () => 42 } + expect(obj == 42).toBe(true) + }) + }) + }) + + describe('Step-by-Step Trace: [] == ![]', () => { + it('should demonstrate [] == ![] is true', () => { + // Step 1: Evaluate ![] + // [] is truthy, so ![] = false + const step1 = ![] + expect(step1).toBe(false) + + // Step 2: Now we have [] == false + // Boolean → Number: false → 0 + // [] == 0 + + // Step 3: Object → Primitive + // [].toString() → "" + // "" == 0 + + // Step 4: String → Number + // "" → 0 + // 0 == 0 → true + + const emptyArray = [] + expect(emptyArray == step1).toBe(true) + }) + }) + }) + + describe('Strict Equality (===)', () => { + describe('Type Check', () => { + it('should return false for different types immediately', () => { + expect(1 === "1").toBe(false) + expect(true === 1).toBe(false) + expect(null === undefined).toBe(false) + }) + }) + + describe('Number Comparison', () => { + it('should compare numeric values', () => { + expect(42 === 42).toBe(true) + expect(Infinity === Infinity).toBe(true) + }) + + it('should return false for NaN === NaN', () => { + expect(NaN === NaN).toBe(false) + }) + + it('should return true for +0 === -0', () => { + expect(+0 === -0).toBe(true) + }) + }) + + describe('String Comparison', () => { + it('should compare string characters', () => { + expect("hello" === "hello").toBe(true) + expect("hello" === "Hello").toBe(false) // Case sensitive + expect("hello" === "hello ").toBe(false) // Different length + }) + }) + + describe('Boolean Comparison', () => { + it('should compare boolean values', () => { + expect(true === true).toBe(true) + expect(false === false).toBe(true) + expect(true === false).toBe(false) + }) + }) + + describe('BigInt Comparison', () => { + it('should compare BigInt values', () => { + expect(10n === 10n).toBe(true) + expect(10n === 20n).toBe(false) + }) + }) + + describe('Symbol Comparison', () => { + it('should return false for different symbols', () => { + const sym = Symbol("id") + expect(sym === sym).toBe(true) + expect(Symbol("id") === Symbol("id")).toBe(false) + }) + }) + + describe('Object Comparison (Reference)', () => { + it('should compare by reference, not value', () => { + const obj = { a: 1 } + expect(obj === obj).toBe(true) + + const obj1 = { a: 1 } + const obj2 = { a: 1 } + expect(obj1 === obj2).toBe(false) // Different objects! + }) + + it('should return false for different arrays', () => { + const arr1 = [1, 2, 3] + const arr2 = [1, 2, 3] + const arr3 = arr1 + + expect(arr1 === arr2).toBe(false) + expect(arr1 === arr3).toBe(true) + }) + + it('should return false for different functions', () => { + const fn1 = () => {} + const fn2 = () => {} + const fn3 = fn1 + + expect(fn1 === fn2).toBe(false) + expect(fn1 === fn3).toBe(true) + }) + }) + + describe('null and undefined', () => { + it('should compare null and undefined correctly', () => { + expect(null === null).toBe(true) + expect(undefined === undefined).toBe(true) + expect(null === undefined).toBe(false) + }) + }) + + describe('Predictable Results', () => { + it('should return false for different types', () => { + expect(1 === "1").toBe(false) + expect(0 === "").toBe(false) + expect(true === 1).toBe(false) + expect(false === 0).toBe(false) + expect(null === undefined).toBe(false) + }) + + it('should return true for same type and value', () => { + expect(1 === 1).toBe(true) + expect("hello" === "hello").toBe(true) + expect(true === true).toBe(true) + expect(null === null).toBe(true) + expect(undefined === undefined).toBe(true) + }) + }) + + describe('Special Cases: NaN and ±0', () => { + it('should demonstrate NaN !== NaN', () => { + expect(NaN === NaN).toBe(false) + expect(Number.isNaN(NaN)).toBe(true) + expect(isNaN(NaN)).toBe(true) + expect(isNaN("hello")).toBe(true) // Converts to NaN first + expect(Number.isNaN("hello")).toBe(false) // No conversion + }) + + it('should demonstrate +0 === -0', () => { + expect(+0 === -0).toBe(true) + expect(1 / +0).toBe(Infinity) + expect(1 / -0).toBe(-Infinity) + expect(Object.is(+0, -0)).toBe(false) + }) + + it('should detect -0', () => { + expect(0 * -1).toBe(-0) + expect(Object.is(0 * -1, -0)).toBe(true) + }) + }) + }) + + describe('Object.is()', () => { + describe('Comparison with ===', () => { + it('should behave like === for most cases', () => { + expect(Object.is(1, 1)).toBe(true) + expect(Object.is("a", "a")).toBe(true) + expect(Object.is(null, null)).toBe(true) + + const obj1 = {} + const obj2 = {} + expect(Object.is(obj1, obj2)).toBe(false) + }) + }) + + describe('NaN Equality', () => { + it('should return true for NaN === NaN', () => { + expect(Object.is(NaN, NaN)).toBe(true) + }) + }) + + describe('±0 Distinction', () => { + it('should distinguish +0 from -0', () => { + expect(Object.is(+0, -0)).toBe(false) + expect(Object.is(-0, 0)).toBe(false) + }) + }) + + describe('Practical Uses', () => { + it('should check for NaN', () => { + function isReallyNaN(value) { + return Object.is(value, NaN) + } + expect(isReallyNaN(NaN)).toBe(true) + expect(isReallyNaN("hello")).toBe(false) + }) + + it('should check for negative zero', () => { + function isNegativeZero(value) { + return Object.is(value, -0) + } + expect(isNegativeZero(-0)).toBe(true) + expect(isNegativeZero(0)).toBe(false) + }) + }) + + describe('Complete Comparison Table', () => { + it('should show differences between ==, ===, and Object.is()', () => { + // 1, "1" + expect(1 == "1").toBe(true) + expect(1 === "1").toBe(false) + expect(Object.is(1, "1")).toBe(false) + + // 0, false + expect(0 == false).toBe(true) + expect(0 === false).toBe(false) + expect(Object.is(0, false)).toBe(false) + + // null, undefined + expect(null == undefined).toBe(true) + expect(null === undefined).toBe(false) + expect(Object.is(null, undefined)).toBe(false) + + // NaN, NaN + expect(NaN == NaN).toBe(false) + expect(NaN === NaN).toBe(false) + expect(Object.is(NaN, NaN)).toBe(true) + + // +0, -0 + expect(+0 == -0).toBe(true) + expect(+0 === -0).toBe(true) + expect(Object.is(+0, -0)).toBe(false) + }) + }) + }) + + describe('typeof Operator', () => { + describe('Correct Results', () => { + it('should return correct types for primitives', () => { + expect(typeof "hello").toBe("string") + expect(typeof 42).toBe("number") + expect(typeof 42n).toBe("bigint") + expect(typeof true).toBe("boolean") + expect(typeof undefined).toBe("undefined") + expect(typeof Symbol()).toBe("symbol") + }) + + it('should return "object" for objects and arrays', () => { + expect(typeof {}).toBe("object") + expect(typeof []).toBe("object") + expect(typeof new Date()).toBe("object") + expect(typeof /regex/).toBe("object") + }) + + it('should return "function" for functions', () => { + expect(typeof function(){}).toBe("function") + expect(typeof (() => {})).toBe("function") + expect(typeof class {}).toBe("function") + expect(typeof Math.sin).toBe("function") + }) + }) + + describe('Famous Quirks', () => { + it('should return "object" for null (bug)', () => { + expect(typeof null).toBe("object") + }) + + it('should return "object" for arrays', () => { + expect(typeof []).toBe("object") + expect(typeof [1, 2, 3]).toBe("object") + expect(typeof new Array()).toBe("object") + }) + + it('should return "number" for NaN', () => { + expect(typeof NaN).toBe("number") + }) + + it('should return "undefined" for undeclared variables', () => { + expect(typeof undeclaredVariable).toBe("undefined") + }) + }) + + describe('Workarounds', () => { + it('should check for null explicitly', () => { + function getType(value) { + if (value === null) return "null" + return typeof value + } + + expect(getType(null)).toBe("null") + expect(getType(undefined)).toBe("undefined") + expect(getType(42)).toBe("number") + }) + + it('should check for "real" objects', () => { + function isRealObject(value) { + return value !== null && typeof value === "object" + } + + expect(isRealObject({})).toBe(true) + expect(isRealObject([])).toBe(true) + expect(isRealObject(null)).toBe(false) + }) + }) + }) + + describe('Better Type Checking Alternatives', () => { + describe('Type-Specific Checks', () => { + it('should use Array.isArray for arrays', () => { + expect(Array.isArray([])).toBe(true) + expect(Array.isArray([1, 2, 3])).toBe(true) + expect(Array.isArray({})).toBe(false) + expect(Array.isArray("hello")).toBe(false) + expect(Array.isArray(null)).toBe(false) + }) + + it('should use Number.isNaN for NaN', () => { + expect(Number.isNaN(NaN)).toBe(true) + expect(Number.isNaN("hello")).toBe(false) + expect(Number.isNaN(undefined)).toBe(false) + }) + + it('should use Number.isFinite for finite numbers', () => { + expect(Number.isFinite(42)).toBe(true) + expect(Number.isFinite(Infinity)).toBe(false) + expect(Number.isFinite(NaN)).toBe(false) + }) + + it('should use Number.isInteger for integers', () => { + expect(Number.isInteger(42)).toBe(true) + expect(Number.isInteger(42.5)).toBe(false) + }) + }) + + describe('instanceof', () => { + it('should check instance of constructor', () => { + expect([] instanceof Array).toBe(true) + expect({} instanceof Object).toBe(true) + expect(new Date() instanceof Date).toBe(true) + expect(/regex/ instanceof RegExp).toBe(true) + }) + + it('should work with custom classes', () => { + class Person {} + const p = new Person() + expect(p instanceof Person).toBe(true) + }) + }) + + describe('Object.prototype.toString', () => { + it('should return precise type information', () => { + const getType = (value) => + Object.prototype.toString.call(value).slice(8, -1) + + expect(getType(null)).toBe("Null") + expect(getType(undefined)).toBe("Undefined") + expect(getType([])).toBe("Array") + expect(getType({})).toBe("Object") + expect(getType(new Date())).toBe("Date") + expect(getType(/regex/)).toBe("RegExp") + expect(getType(new Map())).toBe("Map") + expect(getType(new Set())).toBe("Set") + expect(getType(Promise.resolve())).toBe("Promise") + expect(getType(function(){})).toBe("Function") + expect(getType(42)).toBe("Number") + expect(getType("hello")).toBe("String") + expect(getType(Symbol())).toBe("Symbol") + expect(getType(42n)).toBe("BigInt") + }) + }) + + describe('Custom Type Checker', () => { + it('should create comprehensive type checker', () => { + function getType(value) { + if (value === null) return "null" + + const type = typeof value + if (type !== "object" && type !== "function") { + return type + } + + const tag = Object.prototype.toString.call(value) + return tag.slice(8, -1).toLowerCase() + } + + expect(getType(null)).toBe("null") + expect(getType([])).toBe("array") + expect(getType({})).toBe("object") + expect(getType(new Date())).toBe("date") + expect(getType(/regex/)).toBe("regexp") + expect(getType(new Map())).toBe("map") + expect(getType(Promise.resolve())).toBe("promise") + }) + }) + }) + + describe('Common Gotchas and Mistakes', () => { + describe('Comparing Objects by Value', () => { + it('should demonstrate object reference comparison', () => { + const user1 = { name: "Alice" } + const user2 = { name: "Alice" } + + expect(user1 === user2).toBe(false) // Never runs as equal! + + // Option 1: Compare specific properties + expect(user1.name === user2.name).toBe(true) + + // Option 2: JSON.stringify + expect(JSON.stringify(user1) === JSON.stringify(user2)).toBe(true) + }) + }) + + describe('NaN Comparisons', () => { + it('should never use === for NaN', () => { + const result = parseInt("hello") + + expect(result === NaN).toBe(false) // Never works! + expect(Number.isNaN(result)).toBe(true) // Correct way + expect(Object.is(result, NaN)).toBe(true) // Also works + }) + }) + + describe('typeof null Trap', () => { + it('should handle null separately from objects', () => { + function processObject(obj) { + if (obj !== null && typeof obj === "object") { + return "real object" + } + return "not an object" + } + + expect(processObject({})).toBe("real object") + expect(processObject(null)).toBe("not an object") + }) + }) + + describe('String Comparison Gotchas', () => { + it('should demonstrate string comparison issues', () => { + // Strings compare lexicographically + expect("10" > "9").toBe(false) // "1" < "9" + + // Convert to numbers for numeric comparison + expect(Number("10") > Number("9")).toBe(true) + expect(+"10" > +"9").toBe(true) + }) + }) + + describe('Empty Array Comparisons', () => { + it('should demonstrate array truthiness vs equality', () => { + const arr = [] + + // These seem contradictory + expect(arr == false).toBe(true) + expect(arr ? true : false).toBe(true) // arr is truthy! + + // Check array length instead + expect(arr.length === 0).toBe(true) + expect(!arr.length).toBe(true) + }) + }) + }) + + describe('Decision Guide', () => { + describe('Default to ===', () => { + it('should use === for predictable comparisons', () => { + expect(5 === 5).toBe(true) + expect(5 === "5").toBe(false) // No surprise + }) + }) + + describe('Use == null for Nullish Checks', () => { + it('should check for null or undefined', () => { + function isNullish(value) { + return value == null + } + + expect(isNullish(null)).toBe(true) + expect(isNullish(undefined)).toBe(true) + expect(isNullish(0)).toBe(false) + expect(isNullish("")).toBe(false) + expect(isNullish(false)).toBe(false) + }) + }) + + describe('Use Number.isNaN for NaN', () => { + it('should use Number.isNaN, not isNaN', () => { + expect(Number.isNaN(NaN)).toBe(true) + expect(Number.isNaN("hello")).toBe(false) // Correct + expect(isNaN("hello")).toBe(true) // Wrong! + }) + }) + + describe('Use Array.isArray for Arrays', () => { + it('should use Array.isArray, not typeof', () => { + expect(Array.isArray([])).toBe(true) + expect(typeof []).toBe("object") // Not helpful + }) + }) + + describe('Use Object.is for Edge Cases', () => { + it('should use Object.is for NaN and ±0', () => { + expect(Object.is(NaN, NaN)).toBe(true) + expect(Object.is(+0, -0)).toBe(false) + }) + }) + }) + + describe('Additional Missing Examples', () => { + describe('More Loose Equality Examples', () => { + it('should coerce 42 == "42" to true', () => { + expect(42 == "42").toBe(true) + }) + + it('should return false for undefined == ""', () => { + expect(undefined == "").toBe(false) + }) + }) + + describe('More Strict Equality Examples', () => { + it('should return false for array === string', () => { + const arr = [] + const str = "" + expect(arr === str).toBe(false) + }) + + it('should demonstrate -0 === 0 is true', () => { + expect(-0 === 0).toBe(true) + expect(0 === -0).toBe(true) + }) + }) + + describe('Negative Zero Edge Cases', () => { + it('should demonstrate 1/+0 vs 1/-0', () => { + expect(1 / +0).toBe(Infinity) + expect(1 / -0).toBe(-Infinity) + expect((1 / +0) === (1 / -0)).toBe(false) + }) + + it('should demonstrate Math.sign with -0', () => { + expect(Object.is(Math.sign(-0), -0)).toBe(true) + expect(Math.sign(-0) === 0).toBe(true) // But === says it equals 0 + }) + + it('should parse -0 from JSON', () => { + const negZero = JSON.parse("-0") + expect(Object.is(negZero, -0)).toBe(true) + }) + + it('should create -0 through multiplication', () => { + expect(Object.is(0 * -1, -0)).toBe(true) + expect(Object.is(-0 * 1, -0)).toBe(true) + }) + }) + + describe('Map with NaN as Key', () => { + it('should use NaN as a Map key', () => { + const map = new Map() + + map.set(NaN, "value for NaN") + + // Map uses SameValueZero algorithm, which treats NaN === NaN + expect(map.get(NaN)).toBe("value for NaN") + expect(map.has(NaN)).toBe(true) + }) + + it('should only have one NaN key despite multiple sets', () => { + const map = new Map() + + map.set(NaN, "first") + map.set(NaN, "second") + + expect(map.size).toBe(1) + expect(map.get(NaN)).toBe("second") + }) + }) + + describe('Number.isSafeInteger', () => { + it('should identify safe integers', () => { + expect(Number.isSafeInteger(3)).toBe(true) + expect(Number.isSafeInteger(-3)).toBe(true) + expect(Number.isSafeInteger(0)).toBe(true) + expect(Number.isSafeInteger(Number.MAX_SAFE_INTEGER)).toBe(true) + expect(Number.isSafeInteger(Number.MIN_SAFE_INTEGER)).toBe(true) + }) + + it('should return false for unsafe integers', () => { + expect(Number.isSafeInteger(Number.MAX_SAFE_INTEGER + 1)).toBe(false) + expect(Number.isSafeInteger(Number.MIN_SAFE_INTEGER - 1)).toBe(false) + }) + + it('should return false for non-integers', () => { + expect(Number.isSafeInteger(3.1)).toBe(false) + expect(Number.isSafeInteger(NaN)).toBe(false) + expect(Number.isSafeInteger(Infinity)).toBe(false) + expect(Number.isSafeInteger("3")).toBe(false) + }) + }) + + describe('NaN Creation Examples', () => { + it('should create NaN from 0/0', () => { + expect(Number.isNaN(0 / 0)).toBe(true) + }) + + it('should create NaN from Math.sqrt(-1)', () => { + expect(Number.isNaN(Math.sqrt(-1))).toBe(true) + }) + + it('should create NaN from invalid math operations', () => { + expect(Number.isNaN(Infinity - Infinity)).toBe(true) + expect(Number.isNaN(Infinity / Infinity)).toBe(true) + expect(Number.isNaN(0 * Infinity)).toBe(true) + }) + }) + + describe('Sorting Array of Number Strings', () => { + it('should sort incorrectly with default sort', () => { + const arr = ["10", "9", "2", "1", "100"] + const sorted = [...arr].sort() + + // Lexicographic sort - NOT numeric order! + expect(sorted).toEqual(["1", "10", "100", "2", "9"]) + }) + + it('should sort correctly with numeric comparison', () => { + const arr = ["10", "9", "2", "1", "100"] + const sorted = [...arr].sort((a, b) => Number(a) - Number(b)) + + expect(sorted).toEqual(["1", "2", "9", "10", "100"]) + }) + + it('should sort correctly using + for conversion', () => { + const arr = ["10", "9", "2", "1", "100"] + const sorted = [...arr].sort((a, b) => +a - +b) + + expect(sorted).toEqual(["1", "2", "9", "10", "100"]) + }) + }) + }) +}) diff --git a/tests/fundamentals/javascript-engines/javascript-engines.test.js b/tests/fundamentals/javascript-engines/javascript-engines.test.js new file mode 100644 index 00000000..85f56470 --- /dev/null +++ b/tests/fundamentals/javascript-engines/javascript-engines.test.js @@ -0,0 +1,592 @@ +import { describe, it, expect } from 'vitest' + +describe('JavaScript Engines', () => { + describe('Basic Examples from Documentation', () => { + it('should demonstrate basic function execution (opening example)', () => { + // From the opening of the documentation + function greet(name) { + return "Hello, " + name + "!" + } + + expect(greet("World")).toBe("Hello, World!") + expect(greet("JavaScript")).toBe("Hello, JavaScript!") + expect(greet("V8")).toBe("Hello, V8!") + }) + }) + + describe('Object Shape Consistency', () => { + it('should create objects with consistent property order', () => { + // Objects with same properties in same order share hidden classes + const point1 = { x: 1, y: 2 } + const point2 = { x: 5, y: 10 } + + // Both should have same property keys in same order + expect(Object.keys(point1)).toEqual(['x', 'y']) + expect(Object.keys(point2)).toEqual(['x', 'y']) + expect(Object.keys(point1)).toEqual(Object.keys(point2)) + }) + + it('should show different key order for differently created objects', () => { + // Property order matters for hidden classes + const a = { x: 1, y: 2 } + const b = { y: 2, x: 1 } + + // Keys are in different order + expect(Object.keys(a)).toEqual(['x', 'y']) + expect(Object.keys(b)).toEqual(['y', 'x']) + + // These have DIFFERENT hidden classes in V8! + expect(Object.keys(a)).not.toEqual(Object.keys(b)) + }) + + it('should maintain consistent shapes with factory functions', () => { + // Factory functions create consistent shapes (engine-friendly) + function createPoint(x, y) { + return { x, y } + } + + const p1 = createPoint(1, 2) + const p2 = createPoint(3, 4) + const p3 = createPoint(5, 6) + + // All have identical structure + expect(Object.keys(p1)).toEqual(Object.keys(p2)) + expect(Object.keys(p2)).toEqual(Object.keys(p3)) + }) + + it('should demonstrate transition chains when adding properties', () => { + const obj = {} + expect(Object.keys(obj)).toEqual([]) + + obj.x = 1 + expect(Object.keys(obj)).toEqual(['x']) + + obj.y = 2 + expect(Object.keys(obj)).toEqual(['x', 'y']) + + // Each step creates a new hidden class (transition chain) + }) + + it('should compare Pattern A (object literal) vs Pattern B (empty object + properties)', () => { + // Pattern A: Object literal - creates shape immediately + function createPointA(x, y) { + return { x: x, y: y } + } + + // Pattern B: Empty object + property additions - goes through transitions + function createPointB(x, y) { + const point = {} + point.x = x + point.y = y + return point + } + + const pointA = createPointA(1, 2) + const pointB = createPointB(3, 4) + + // Both produce same final shape + expect(Object.keys(pointA)).toEqual(['x', 'y']) + expect(Object.keys(pointB)).toEqual(['x', 'y']) + + // Both work correctly + expect(pointA.x).toBe(1) + expect(pointA.y).toBe(2) + expect(pointB.x).toBe(3) + expect(pointB.y).toBe(4) + + // Pattern A is more engine-friendly because: + // - V8 can optimize object literals with known properties + // - Pattern B goes through 3 hidden class transitions: {} -> {x} -> {x,y} + }) + }) + + describe('Type Consistency', () => { + it('should demonstrate consistent number operations', () => { + // V8 optimizes for consistent types + function add(a, b) { + return a + b + } + + // Consistent number usage (monomorphic, fast) + expect(add(1, 2)).toBe(3) + expect(add(3, 4)).toBe(7) + expect(add(5, 6)).toBe(11) + }) + + it('should handle type changes (triggers deoptimization)', () => { + function process(x) { + return x + x + } + + // Numbers + expect(process(5)).toBe(10) + expect(process(10)).toBe(20) + + // Strings (type change - would trigger deoptimization in V8) + expect(process("hello")).toBe("hellohello") + + // Mixed usage works but is slower due to deoptimization + }) + + it('should demonstrate dynamic object shapes with process function', () => { + // From JIT compilation section - shows why JS needs JIT + function process(x) { + return x.value * 2 + } + + // Object with number value + expect(process({ value: 10 })).toBe(20) + + // Object with string value (NaN result) + expect(process({ value: "hello" })).toBeNaN() + + // Different shape (extra property) - still works + expect(process({ value: 10, extra: 5 })).toBe(20) + + // Even more different shape + expect(process({ value: 5, a: 1, b: 2 })).toBe(10) + + // This demonstrates why JavaScript needs JIT: + // - x could be any object shape + // - x.value could be any type + // - AOT compilation can't optimize for all possibilities + }) + + it('should show typeof consistency', () => { + let num = 42 + expect(typeof num).toBe('number') + + // Changing types is valid JS but can cause deoptimization + // let num = "forty-two" // Would change type + + // Better: use separate variables + const numValue = 42 + const strValue = "forty-two" + + expect(typeof numValue).toBe('number') + expect(typeof strValue).toBe('string') + }) + }) + + describe('Array Optimization', () => { + it('should create dense arrays (engine-friendly)', () => { + // Dense array - all indices filled, same type + const dense = [1, 2, 3, 4, 5] + + expect(dense.length).toBe(5) + expect(dense[0]).toBe(1) + expect(dense[4]).toBe(5) + + // V8 can use optimized "packed" array representation + }) + + it('should demonstrate sparse arrays (slower)', () => { + // Sparse array with holes - V8 uses slower dictionary mode + const sparse = [] + sparse[0] = 1 + sparse[100] = 2 + + expect(sparse.length).toBe(101) + expect(sparse[0]).toBe(1) + expect(sparse[50]).toBe(undefined) // Hole + expect(sparse[100]).toBe(2) + + // This creates 99 "holes" - less efficient + }) + + it('should show typed array benefits', () => { + // Typed arrays are always optimized (single type, no holes) + const int32Array = new Int32Array([1, 2, 3, 4, 5]) + + expect(int32Array.length).toBe(5) + expect(int32Array[0]).toBe(1) + + // All elements guaranteed to be 32-bit integers + }) + + it('should demonstrate mixed-type arrays (polymorphic)', () => { + // Mixed types require more generic handling + const mixed = [1, "two", 3, null, { four: 4 }] + + expect(typeof mixed[0]).toBe('number') + expect(typeof mixed[1]).toBe('string') + expect(typeof mixed[3]).toBe('object') + expect(typeof mixed[4]).toBe('object') + + // V8 can't assume element types - slower operations + }) + + it('should preserve array type with consistent operations', () => { + const numbers = [1, 2, 3, 4, 5] + + // map preserves array structure + const doubled = numbers.map(n => n * 2) + expect(doubled).toEqual([2, 4, 6, 8, 10]) + + // filter preserves type consistency + const filtered = numbers.filter(n => n > 2) + expect(filtered).toEqual([3, 4, 5]) + }) + }) + + describe('Property Access Patterns', () => { + it('should demonstrate monomorphic property access', () => { + // Monomorphic: always same object shape + function getX(obj) { + return obj.x + } + + // All objects have same shape - fastest IC state + expect(getX({ x: 1, y: 2 })).toBe(1) + expect(getX({ x: 3, y: 4 })).toBe(3) + expect(getX({ x: 5, y: 6 })).toBe(5) + }) + + it('should show polymorphic access (multiple shapes)', () => { + function getX(obj) { + return obj.x + } + + // Different shapes - polymorphic IC + expect(getX({ x: 1 })).toBe(1) // Shape A + expect(getX({ x: 2, y: 3 })).toBe(2) // Shape B + expect(getX({ x: 4, y: 5, z: 6 })).toBe(4) // Shape C + + // Still works, but inline cache has multiple entries + }) + + it('should demonstrate computed property access', () => { + const obj = { a: 1, b: 2, c: 3 } + + // Direct property access (faster) + expect(obj.a).toBe(1) + + // Computed property access (slightly slower but necessary for dynamic keys) + const key = 'b' + expect(obj[key]).toBe(2) + }) + + it('should demonstrate megamorphic access (many different shapes)', () => { + function getX(obj) { + return obj.x + } + + // Every call has a completely different shape + // This would cause megamorphic IC state in V8 + expect(getX({ x: 1 })).toBe(1) + expect(getX({ x: 2, a: 1 })).toBe(2) + expect(getX({ x: 3, b: 2 })).toBe(3) + expect(getX({ x: 4, c: 3 })).toBe(4) + expect(getX({ x: 5, d: 4 })).toBe(5) + expect(getX({ x: 6, e: 5 })).toBe(6) + expect(getX({ x: 7, f: 6 })).toBe(7) + + // IC gives up after too many shapes - falls back to generic lookup + // Still works correctly, just slower than monomorphic/polymorphic + }) + }) + + describe('Class vs Object Literal Shapes', () => { + it('should create consistent shapes with classes', () => { + class Point { + constructor(x, y) { + this.x = x + this.y = y + } + } + + const p1 = new Point(1, 2) + const p2 = new Point(3, 4) + const p3 = new Point(5, 6) + + // All instances have identical shape + expect(Object.keys(p1)).toEqual(['x', 'y']) + expect(Object.keys(p2)).toEqual(['x', 'y']) + expect(Object.keys(p3)).toEqual(['x', 'y']) + }) + + it('should show prototype chain optimization', () => { + class Animal { + speak() { + return 'sound' + } + } + + class Dog extends Animal { + speak() { + return 'woof' + } + } + + const dog = new Dog() + + // Method lookup follows prototype chain + expect(dog.speak()).toBe('woof') + expect(dog instanceof Dog).toBe(true) + expect(dog instanceof Animal).toBe(true) + }) + }) + + describe('Avoiding Deoptimization Patterns', () => { + it('should show delete causing shape change', () => { + const user = { name: 'Alice', age: 30, temp: true } + + expect(Object.keys(user)).toEqual(['name', 'age', 'temp']) + + // delete changes hidden class (bad for performance) + delete user.temp + + expect(Object.keys(user)).toEqual(['name', 'age']) + expect(user.temp).toBe(undefined) + + // Better alternative: set to undefined + const user2 = { name: 'Bob', age: 25, temp: true } + user2.temp = undefined // Hidden class stays the same + + expect('temp' in user2).toBe(true) // Property still exists + expect(user2.temp).toBe(undefined) + }) + + it('should demonstrate object spread for immutable updates', () => { + const original = { x: 1, y: 2, z: 3 } + + // Instead of mutating, create new object + const updated = { ...original, z: 10 } + + expect(original.z).toBe(3) // Original unchanged + expect(updated.z).toBe(10) // New object with update + + // Both have consistent shapes + expect(Object.keys(original)).toEqual(['x', 'y', 'z']) + expect(Object.keys(updated)).toEqual(['x', 'y', 'z']) + }) + + it('should show inconsistent shapes with conditional property assignment', () => { + // Bad pattern: conditional property assignment creates different shapes + function createUserBad(name, age) { + const user = {} + if (name) user.name = name + if (age) user.age = age + return user + } + + const user1 = createUserBad('Alice', 30) + const user2 = createUserBad('Bob', null) // Only name + const user3 = createUserBad(null, 25) // Only age + const user4 = createUserBad(null, null) // Empty + + // All have different shapes! + expect(Object.keys(user1)).toEqual(['name', 'age']) + expect(Object.keys(user2)).toEqual(['name']) + expect(Object.keys(user3)).toEqual(['age']) + expect(Object.keys(user4)).toEqual([]) + + // Compare with good pattern + function createUserGood(name, age) { + return { name, age } // Always same shape + } + + const goodUser1 = createUserGood('Alice', 30) + const goodUser2 = createUserGood('Bob', null) + const goodUser3 = createUserGood(null, 25) + + // Same shape regardless of values (nulls are still properties) + expect(Object.keys(goodUser1)).toEqual(['name', 'age']) + expect(Object.keys(goodUser2)).toEqual(['name', 'age']) + expect(Object.keys(goodUser3)).toEqual(['name', 'age']) + }) + }) + + describe('Function Optimization Patterns', () => { + it('should demonstrate consistent function signatures', () => { + function multiply(a, b) { + return a * b + } + + // Consistent argument types enable optimization + expect(multiply(2, 3)).toBe(6) + expect(multiply(4, 5)).toBe(20) + expect(multiply(6, 7)).toBe(42) + }) + + it('should show inlining with small functions', () => { + // Small functions are candidates for inlining + function square(x) { + return x * x + } + + function sumOfSquares(a, b) { + return square(a) + square(b) + } + + // V8 may inline square() into sumOfSquares() + expect(sumOfSquares(3, 4)).toBe(25) // 9 + 16 + }) + + it('should demonstrate closure optimization', () => { + function createAdder(x) { + // Closure captures x + return function(y) { + return x + y + } + } + + const add5 = createAdder(5) + const add10 = createAdder(10) + + // Closures with consistent captured values can be optimized + expect(add5(3)).toBe(8) + expect(add10(3)).toBe(13) + }) + }) + + describe('Garbage Collection Concepts', () => { + it('should demonstrate object references', () => { + let obj = { data: 'important' } + const ref = obj + + // Both point to same object + expect(ref.data).toBe('important') + + // Setting obj to null doesn't GC the object + // because ref still holds a reference + obj = null + expect(ref.data).toBe('important') + }) + + it('should show circular references', () => { + const a = { name: 'a' } + const b = { name: 'b' } + + // Circular reference + a.ref = b + b.ref = a + + expect(a.ref.name).toBe('b') + expect(b.ref.name).toBe('a') + expect(a.ref.ref.name).toBe('a') + + // Modern GC can handle circular references + // (mark-and-sweep doesn't rely on reference counting) + }) + + it('should demonstrate WeakRef for GC-friendly references', () => { + // WeakRef allows object to be garbage collected + let obj = { data: 'temporary' } + const weakRef = new WeakRef(obj) + + // Can access while object exists + expect(weakRef.deref()?.data).toBe('temporary') + + // Note: We can't force GC in tests, but WeakRef + // allows the referenced object to be collected + }) + + it('should show Map vs WeakMap for memory management', () => { + // Regular Map holds strong references + const map = new Map() + let key = { id: 1 } + map.set(key, 'value') + + expect(map.get(key)).toBe('value') + + // WeakMap allows keys to be garbage collected + const weakMap = new WeakMap() + let weakKey = { id: 2 } + weakMap.set(weakKey, 'value') + + expect(weakMap.get(weakKey)).toBe('value') + + // If weakKey is set to null and no other references exist, + // the entry can be garbage collected + }) + }) + + describe('JIT Compilation Observable Behavior', () => { + it('should handle hot function calls', () => { + function hotFunction(n) { + return n * 2 + } + + // Simulating many calls (would trigger JIT in real V8) + let result = 0 + for (let i = 0; i < 1000; i++) { + result = hotFunction(i) + } + + expect(result).toBe(1998) // Last iteration: 999 * 2 + }) + + it('should demonstrate deoptimization scenario', () => { + function add(a, b) { + return a + b + } + + // Many calls with numbers (would be optimized for numbers) + for (let i = 0; i < 100; i++) { + add(i, i + 1) + } + + // Then a call with strings (triggers deoptimization) + const result = add('hello', 'world') + + // Still produces correct result despite deoptimization + expect(result).toBe('helloworld') + }) + + it('should show consistent returns for optimization', () => { + // Always returns same type (optimizer-friendly) + function maybeDouble(n, shouldDouble) { + if (shouldDouble) { + return n * 2 + } + return n // Always returns number + } + + expect(maybeDouble(5, true)).toBe(10) + expect(maybeDouble(5, false)).toBe(5) + expect(typeof maybeDouble(5, true)).toBe('number') + expect(typeof maybeDouble(5, false)).toBe('number') + }) + }) + + describe('Hidden Class Interview Questions', () => { + it('should explain why object literal order matters', () => { + // Creating objects with different property orders + function createA() { + return { first: 1, second: 2 } + } + + function createB() { + return { second: 2, first: 1 } + } + + const objA = createA() + const objB = createB() + + // Same values, but different hidden classes + expect(objA.first).toBe(objB.first) + expect(objA.second).toBe(objB.second) + + // Property order is different + expect(Object.keys(objA)[0]).toBe('first') + expect(Object.keys(objB)[0]).toBe('second') + }) + + it('should demonstrate best practice with constructor pattern', () => { + // Constructor ensures consistent shape + function User(name, email, age) { + this.name = name + this.email = email + this.age = age + } + + const user1 = new User('Alice', 'alice@example.com', 30) + const user2 = new User('Bob', 'bob@example.com', 25) + + // Guaranteed same property order + expect(Object.keys(user1)).toEqual(['name', 'email', 'age']) + expect(Object.keys(user2)).toEqual(['name', 'email', 'age']) + }) + }) +}) diff --git a/tests/fundamentals/primitive-types/primitive-types.test.js b/tests/fundamentals/primitive-types/primitive-types.test.js new file mode 100644 index 00000000..3e1aea67 --- /dev/null +++ b/tests/fundamentals/primitive-types/primitive-types.test.js @@ -0,0 +1,563 @@ +import { describe, it, expect } from 'vitest' + +describe('Primitive Types', () => { + describe('String', () => { + it('should create strings with single quotes, double quotes, and backticks', () => { + let single = 'Hello' + let double = "World" + let backtick = `Hello World` + + expect(single).toBe('Hello') + expect(double).toBe('World') + expect(backtick).toBe('Hello World') + }) + + it('should support template literal interpolation', () => { + let name = "Alice" + let age = 25 + + let greeting = `Hello, ${name}! You are ${age} years old.` + expect(greeting).toBe("Hello, Alice! You are 25 years old.") + }) + + it('should support multi-line strings with template literals', () => { + let multiLine = ` + This is line 1 + This is line 2 +` + expect(multiLine).toContain('This is line 1') + expect(multiLine).toContain('This is line 2') + }) + + it('should demonstrate string immutability - cannot change characters', () => { + let str = "hello" + // In strict mode, this throws TypeError + // In non-strict mode, it silently fails + expect(() => { + "use strict" + str[0] = "H" + }).toThrow(TypeError) + expect(str).toBe("hello") // Still "hello" + }) + + it('should create new string when "changing" with concatenation', () => { + let str = "hello" + str = "H" + str.slice(1) + expect(str).toBe("Hello") + }) + + it('should not modify original string with toUpperCase', () => { + let name = "Alice" + name.toUpperCase() // Creates "ALICE" but doesn't change 'name' + expect(name).toBe("Alice") // Still "Alice" + }) + }) + + describe('Number', () => { + it('should handle integers, decimals, negatives, and scientific notation', () => { + let integer = 42 + let decimal = 3.14 + let negative = -10 + let scientific = 2.5e6 + + expect(integer).toBe(42) + expect(decimal).toBe(3.14) + expect(negative).toBe(-10) + expect(scientific).toBe(2500000) + }) + + it('should return Infinity for division by zero', () => { + expect(1 / 0).toBe(Infinity) + expect(-1 / 0).toBe(-Infinity) + }) + + it('should return NaN for invalid operations', () => { + expect(Number.isNaN("hello" * 2)).toBe(true) + }) + + it('should demonstrate floating-point precision problem', () => { + expect(0.1 + 0.2).not.toBe(0.3) + expect(0.1 + 0.2).toBeCloseTo(0.3) + expect(0.1 + 0.2 === 0.3).toBe(false) + }) + + it('should have MAX_SAFE_INTEGER and MIN_SAFE_INTEGER', () => { + expect(Number.MAX_SAFE_INTEGER).toBe(9007199254740991) + expect(Number.MIN_SAFE_INTEGER).toBe(-9007199254740991) + }) + + it('should lose precision beyond safe integer range', () => { + // This demonstrates the precision loss + expect(9007199254740992 === 9007199254740993).toBe(true) // Wrong but expected + }) + }) + + describe('BigInt', () => { + it('should create BigInt with n suffix', () => { + let big = 9007199254740993n + expect(big).toBe(9007199254740993n) + }) + + it('should create BigInt from string', () => { + let alsoBig = BigInt("9007199254740993") + expect(alsoBig).toBe(9007199254740993n) + }) + + it('should perform accurate math with BigInt', () => { + let big = 9007199254740993n + expect(big + 1n).toBe(9007199254740994n) + }) + + it('should require explicit conversion between BigInt and Number', () => { + let big = 10n + let regular = 5 + + expect(big + BigInt(regular)).toBe(15n) + expect(Number(big) + regular).toBe(15) + }) + + it('should throw TypeError when mixing BigInt and Number without conversion', () => { + let big = 10n + let regular = 5 + + expect(() => big + regular).toThrow(TypeError) + }) + }) + + describe('Boolean', () => { + it('should have only two values: true and false', () => { + let isLoggedIn = true + let hasPermission = false + + expect(isLoggedIn).toBe(true) + expect(hasPermission).toBe(false) + }) + + it('should create boolean from comparisons', () => { + let age = 25 + let name = "Alice" + + let isAdult = age >= 18 + let isEqual = name === "Alice" + + expect(isAdult).toBe(true) + expect(isEqual).toBe(true) + }) + + describe('Falsy Values', () => { + it('should identify all 8 falsy values', () => { + expect(Boolean(false)).toBe(false) + expect(Boolean(0)).toBe(false) + expect(Boolean(-0)).toBe(false) + expect(Boolean(0n)).toBe(false) + expect(Boolean("")).toBe(false) + expect(Boolean(null)).toBe(false) + expect(Boolean(undefined)).toBe(false) + expect(Boolean(NaN)).toBe(false) + }) + }) + + describe('Truthy Values', () => { + it('should identify truthy values including surprises', () => { + expect(Boolean(true)).toBe(true) + expect(Boolean(1)).toBe(true) + expect(Boolean(-1)).toBe(true) + expect(Boolean("hello")).toBe(true) + expect(Boolean("0")).toBe(true) // Non-empty string is truthy! + expect(Boolean("false")).toBe(true) // Non-empty string is truthy! + expect(Boolean([])).toBe(true) // Empty array is truthy! + expect(Boolean({})).toBe(true) // Empty object is truthy! + expect(Boolean(function(){})).toBe(true) + expect(Boolean(Infinity)).toBe(true) + expect(Boolean(-Infinity)).toBe(true) + }) + }) + + it('should convert to boolean using Boolean() and double negation', () => { + let value = "hello" + let bool = Boolean(value) + let shortcut = !!value + + expect(bool).toBe(true) + expect(shortcut).toBe(true) + }) + }) + + describe('undefined', () => { + it('should be the default value for uninitialized variables', () => { + let x + expect(x).toBe(undefined) + }) + + it('should be the value for missing function parameters', () => { + function greet(name) { + return name + } + expect(greet()).toBe(undefined) + }) + + it('should be the return value of functions without return statement', () => { + function doNothing() { + // no return + } + expect(doNothing()).toBe(undefined) + }) + + it('should be the value for non-existent object properties', () => { + let person = { name: "Alice" } + expect(person.age).toBe(undefined) + }) + }) + + describe('null', () => { + it('should represent intentional absence of value', () => { + let user = { name: "Alice" } + user = null + expect(user).toBe(null) + }) + + it('should be used to indicate no result from functions', () => { + function findUser(id) { + // Simulating user not found + return null + } + expect(findUser(999)).toBe(null) + }) + + it('should have typeof return "object" (famous bug)', () => { + expect(typeof null).toBe("object") + }) + + it('should be checked with strict equality', () => { + let value = null + expect(value === null).toBe(true) + }) + }) + + describe('Symbol', () => { + it('should create unique symbols even with same description', () => { + let id1 = Symbol("id") + let id2 = Symbol("id") + + expect(id1 === id2).toBe(false) + }) + + it('should have accessible description', () => { + let id1 = Symbol("id") + expect(id1.description).toBe("id") + }) + + it('should work as unique object keys', () => { + const ID = Symbol("id") + const user = { + name: "Alice", + [ID]: 12345 + } + + expect(user.name).toBe("Alice") + expect(user[ID]).toBe(12345) + }) + + it('should not appear in Object.keys', () => { + const ID = Symbol("id") + const user = { + name: "Alice", + [ID]: 12345 + } + + expect(Object.keys(user)).toEqual(["name"]) + }) + }) + + describe('typeof Operator', () => { + it('should return correct types for primitives', () => { + expect(typeof "hello").toBe("string") + expect(typeof 42).toBe("number") + expect(typeof 42n).toBe("bigint") + expect(typeof true).toBe("boolean") + expect(typeof undefined).toBe("undefined") + expect(typeof Symbol()).toBe("symbol") + }) + + it('should return "object" for null (bug)', () => { + expect(typeof null).toBe("object") + }) + + it('should return "object" for objects and arrays', () => { + expect(typeof {}).toBe("object") + expect(typeof []).toBe("object") + }) + + it('should return "function" for functions', () => { + expect(typeof function(){}).toBe("function") + }) + }) + + describe('Immutability', () => { + it('should not modify original string with methods', () => { + let str = "hello" + + str.toUpperCase() // Returns "HELLO" + expect(str).toBe("hello") // Still "hello"! + }) + + it('should require reassignment to capture new value', () => { + let str = "hello" + str = str.toUpperCase() + expect(str).toBe("HELLO") + }) + }) + + describe('const vs Immutability', () => { + it('should prevent reassignment with const', () => { + const name = "Alice" + // name = "Bob" would throw TypeError + expect(name).toBe("Alice") + }) + + it('should allow mutation of const objects', () => { + const person = { name: "Alice" } + person.name = "Bob" // Works! + person.age = 25 // Works! + + expect(person.name).toBe("Bob") + expect(person.age).toBe(25) + }) + + it('should demonstrate primitives are immutable regardless of const/let', () => { + let str = "hello" + // In strict mode (which Vitest uses), this throws TypeError + // In non-strict mode, it silently fails + expect(() => { + str[0] = "H" + }).toThrow(TypeError) + expect(str).toBe("hello") + }) + }) + + describe('Autoboxing', () => { + it('should allow calling methods on primitive strings', () => { + expect("hello".toUpperCase()).toBe("HELLO") + }) + + it('should not modify the original primitive when calling methods', () => { + let str = "hello" + str.toUpperCase() + expect(str).toBe("hello") + }) + + it('should demonstrate wrapper objects are different from primitives', () => { + let strObj = new String("hello") + expect(typeof strObj).toBe("object") // Not "string"! + expect(strObj === "hello").toBe(false) // Object vs primitive + }) + + it('should create primitive strings, not wrapper objects', () => { + let str = "hello" + expect(typeof str).toBe("string") + }) + }) + + describe('null vs undefined Comparison', () => { + it('should show loose equality between null and undefined', () => { + expect(null == undefined).toBe(true) + }) + + it('should show strict inequality between null and undefined', () => { + expect(null === undefined).toBe(false) + }) + + it('should demonstrate checking for nullish values', () => { + let value = null + expect(value == null).toBe(true) + + value = undefined + expect(value == null).toBe(true) + }) + + it('should check for specific null', () => { + let value = null + expect(value === null).toBe(true) + }) + + it('should check for specific undefined', () => { + let value = undefined + expect(value === undefined).toBe(true) + }) + + it('should check for "has a value" (not null/undefined)', () => { + let value = "hello" + expect(value != null).toBe(true) + + value = 0 // 0 is a value, not nullish + expect(value != null).toBe(true) + }) + }) + + describe('JavaScript Quirks', () => { + it('should demonstrate NaN is not equal to itself', () => { + expect(NaN === NaN).toBe(false) + expect(NaN !== NaN).toBe(true) + }) + + it('should use Number.isNaN to check for NaN', () => { + expect(Number.isNaN(NaN)).toBe(true) + expect(Number.isNaN("hello")).toBe(false) + expect(isNaN("hello")).toBe(true) // Has quirks + }) + + it('should demonstrate empty string is falsy but whitespace is truthy', () => { + expect(Boolean("")).toBe(false) + expect(Boolean(" ")).toBe(true) + expect(Boolean("0")).toBe(true) + }) + + it('should demonstrate + operator string concatenation', () => { + expect(1 + 2).toBe(3) + expect("1" + "2").toBe("12") + expect(1 + "2").toBe("12") + expect("1" + 2).toBe("12") + expect(1 + 2 + "3").toBe("33") + expect("1" + 2 + 3).toBe("123") + }) + + it('should force number addition with explicit conversion', () => { + expect(Number("1") + Number("2")).toBe(3) + expect(parseInt("1") + parseInt("2")).toBe(3) + }) + + it('should force string concatenation with explicit conversion', () => { + expect(String(1) + String(2)).toBe("12") + expect(`${1}${2}`).toBe("12") + }) + }) + + describe('Type Checking Best Practices', () => { + it('should check for null explicitly', () => { + let value = null + expect(value === null).toBe(true) + }) + + it('should use Array.isArray for arrays', () => { + expect(Array.isArray([1, 2, 3])).toBe(true) + expect(Array.isArray("hello")).toBe(false) + }) + + it('should use Object.prototype.toString for precise type', () => { + expect(Object.prototype.toString.call(null)).toBe("[object Null]") + expect(Object.prototype.toString.call([])).toBe("[object Array]") + expect(Object.prototype.toString.call(new Date())).toBe("[object Date]") + }) + }) + + describe('Intl.NumberFormat for Currency', () => { + it('should format currency correctly with Intl.NumberFormat', () => { + const formatter = new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + }) + + expect(formatter.format(0.30)).toBe("$0.30") + expect(formatter.format(19.99)).toBe("$19.99") + expect(formatter.format(1000)).toBe("$1,000.00") + }) + + it('should handle different locales', () => { + const euroFormatter = new Intl.NumberFormat('de-DE', { + style: 'currency', + currency: 'EUR', + }) + + // German locale uses comma for decimal and period for thousands + expect(euroFormatter.format(1234.56)).toContain("1.234,56") + }) + }) + + describe('Floating-Point Solutions', () => { + it('should use Number.EPSILON for floating-point comparison', () => { + const result = 0.1 + 0.2 + const expected = 0.3 + + // Using epsilon comparison for floating-point + expect(Math.abs(result - expected) < Number.EPSILON).toBe(true) + }) + + it('should use toFixed for rounding display', () => { + expect((0.1 + 0.2).toFixed(2)).toBe("0.30") + expect((0.1 + 0.2).toFixed(1)).toBe("0.3") + }) + + it('should use integers (cents) for precise money calculations', () => { + // Instead of 0.1 + 0.2, use cents + const price1 = 10 // 10 cents + const price2 = 20 // 20 cents + const total = price1 + price2 + + expect(total).toBe(30) // Exactly 30 cents + expect(total / 100).toBe(0.3) // $0.30 + }) + }) + + describe('Array Holes', () => { + it('should return undefined for array holes', () => { + let arr = [1, , 3] // Sparse array with hole at index 1 + + expect(arr[0]).toBe(1) + expect(arr[1]).toBe(undefined) + expect(arr[2]).toBe(3) + expect(arr.length).toBe(3) + }) + + it('should skip holes in forEach but include in map', () => { + let arr = [1, , 3] + let forEachCount = 0 + let mapResult + + arr.forEach(() => forEachCount++) + mapResult = arr.map(x => x * 2) + + expect(forEachCount).toBe(2) // Holes are skipped + expect(mapResult).toEqual([2, undefined, 6]) // Hole becomes undefined in map result + }) + }) + + describe('String Trim for Empty/Whitespace Check', () => { + it('should use trim to check for empty or whitespace-only strings', () => { + expect("".trim() === "").toBe(true) + expect(" ".trim() === "").toBe(true) + expect("\t\n".trim() === "").toBe(true) + expect("hello".trim() === "").toBe(false) + expect(" hello ".trim() === "").toBe(false) + }) + + it('should use trim with length check for validation', () => { + function isEmptyOrWhitespace(str) { + return str.trim().length === 0 + } + + expect(isEmptyOrWhitespace("")).toBe(true) + expect(isEmptyOrWhitespace(" ")).toBe(true) + expect(isEmptyOrWhitespace("hello")).toBe(false) + }) + }) + + describe('null vs undefined Patterns', () => { + it('should demonstrate clearing with null vs undefined', () => { + let user = { name: "Alice" } + + // Clear intentionally with null + user = null + expect(user).toBe(null) + }) + + it('should show function returning null for no result', () => { + function findUser(id) { + const users = [{ id: 1, name: "Alice" }] + return users.find(u => u.id === id) || null + } + + expect(findUser(1)).toEqual({ id: 1, name: "Alice" }) + expect(findUser(999)).toBe(null) + }) + }) +}) diff --git a/tests/fundamentals/scope-and-closures/scope-and-closures.test.js b/tests/fundamentals/scope-and-closures/scope-and-closures.test.js new file mode 100644 index 00000000..5d1719ab --- /dev/null +++ b/tests/fundamentals/scope-and-closures/scope-and-closures.test.js @@ -0,0 +1,759 @@ +import { describe, it, expect } from 'vitest' + +describe('Scope and Closures', () => { + describe('Scope Basics', () => { + describe('Preventing Naming Conflicts', () => { + it('should keep variables separate in different functions', () => { + function countApples() { + let count = 0 + count++ + return count + } + + function countOranges() { + let count = 0 + count++ + return count + } + + expect(countApples()).toBe(1) + expect(countOranges()).toBe(1) + }) + }) + + describe('Memory Management', () => { + it('should demonstrate scope cleanup concept', () => { + function processData() { + let hugeArray = new Array(1000).fill('x') + return hugeArray.length + } + + // hugeArray can be garbage collected after function returns + expect(processData()).toBe(1000) + }) + }) + + describe('Encapsulation', () => { + it('should hide implementation details', () => { + function createBankAccount() { + let balance = 0 + + return { + deposit(amount) { balance += amount }, + getBalance() { return balance } + } + } + + const account = createBankAccount() + account.deposit(100) + expect(account.getBalance()).toBe(100) + // balance is private - cannot access directly + }) + }) + }) + + describe('The Three Types of Scope', () => { + describe('Global Scope', () => { + it('should access global variables from anywhere', () => { + const appName = "MyApp" + let userCount = 0 + + function greet() { + userCount++ + return appName + } + + expect(greet()).toBe("MyApp") + expect(userCount).toBe(1) + }) + }) + + describe('Function Scope', () => { + it('should keep var variables within function', () => { + function calculateTotal() { + var subtotal = 100 + var tax = 10 + var total = subtotal + tax + return total + } + + expect(calculateTotal()).toBe(110) + // subtotal, tax, total are not accessible here + }) + + describe('var Hoisting', () => { + it('should demonstrate var hoisting behavior', () => { + function example() { + const first = message // undefined (not an error!) + var message = "Hello" + const second = message // "Hello" + return { first, second } + } + + const result = example() + expect(result.first).toBe(undefined) + expect(result.second).toBe("Hello") + }) + }) + }) + + describe('Block Scope', () => { + it('should keep let and const within blocks', () => { + let outsideBlock = "outside" + + if (true) { + let blockLet = "I'm block-scoped" + const blockConst = "Me too" + var functionVar = "I escape the block!" + outsideBlock = blockLet // Can access from inside + } + + expect(functionVar).toBe("I escape the block!") + expect(outsideBlock).toBe("I'm block-scoped") + // blockLet and blockConst are not accessible here + }) + + describe('Temporal Dead Zone', () => { + it('should throw ReferenceError when accessing let before declaration', () => { + function demo() { + // TDZ for 'name' starts here + const getName = () => name // This creates closure over TDZ variable + + let name = "Alice" // TDZ ends here + return name + } + + expect(demo()).toBe("Alice") + }) + + it('should demonstrate proper let declaration', () => { + function demo() { + let name = "Alice" + return name + } + + expect(demo()).toBe("Alice") + }) + }) + }) + }) + + describe('var vs let vs const', () => { + describe('Redeclaration', () => { + it('should allow var redeclaration', () => { + var name = "Alice" + var name = "Bob" // No error, silently overwrites + expect(name).toBe("Bob") + }) + + // Note: let and const redeclaration would cause SyntaxError + // which cannot be tested at runtime + }) + + describe('Reassignment', () => { + it('should allow var and let reassignment', () => { + var count = 1 + count = 2 + expect(count).toBe(2) + + let score = 100 + score = 200 + expect(score).toBe(200) + }) + + it('should allow const object mutation but not reassignment', () => { + const user = { name: "Alice" } + user.name = "Bob" // Works! + user.age = 25 // Works! + + expect(user.name).toBe("Bob") + expect(user.age).toBe(25) + // user = {} would throw TypeError + }) + }) + + describe('The Classic for-loop Problem', () => { + it('should demonstrate var problem with setTimeout', async () => { + const results = [] + + // Using var - only ONE i variable shared + for (var i = 0; i < 3; i++) { + // Capture current value using IIFE + ((j) => { + setTimeout(() => { + results.push(j) + }, 10) + })(i) + } + + await new Promise(resolve => setTimeout(resolve, 50)) + expect(results.sort()).toEqual([0, 1, 2]) + }) + + it('should demonstrate let solution with setTimeout', async () => { + const results = [] + + // Using let - each iteration gets its OWN i variable + for (let i = 0; i < 3; i++) { + setTimeout(() => { + results.push(i) + }, 10) + } + + await new Promise(resolve => setTimeout(resolve, 50)) + expect(results.sort()).toEqual([0, 1, 2]) + }) + }) + }) + + describe('Lexical Scope', () => { + it('should access variables from outer scopes', () => { + const outer = "I'm outside!" + + function outerFunction() { + const middle = "I'm in the middle!" + + function innerFunction() { + const inner = "I'm inside!" + return { inner, middle, outer } + } + + return innerFunction() + } + + const result = outerFunction() + expect(result.inner).toBe("I'm inside!") + expect(result.middle).toBe("I'm in the middle!") + expect(result.outer).toBe("I'm outside!") + }) + + describe('Scope Chain', () => { + it('should walk up the scope chain to find variables', () => { + const x = "global" + + function outer() { + const x = "outer" + + function inner() { + // Looks for x in inner scope first (not found) + // Then looks in outer scope (found!) + return x + } + + return inner() + } + + expect(outer()).toBe("outer") + }) + }) + + describe('Variable Shadowing', () => { + it('should shadow outer variables with inner declarations', () => { + const name = "Global" + + function greet() { + const name = "Function" // Shadows global 'name' + + function inner() { + const name = "Block" // Shadows function 'name' + return name + } + + return { inner: inner(), outer: name } + } + + const result = greet() + expect(result.inner).toBe("Block") + expect(result.outer).toBe("Function") + expect(name).toBe("Global") + }) + }) + }) + + describe('Closures', () => { + describe('Basic Closure', () => { + it('should remember variables from outer scope', () => { + function createGreeter(greeting) { + return function(name) { + return `${greeting}, ${name}!` + } + } + + const sayHello = createGreeter("Hello") + const sayHola = createGreeter("Hola") + + expect(sayHello("Alice")).toBe("Hello, Alice!") + expect(sayHola("Bob")).toBe("Hola, Bob!") + }) + }) + + describe('Data Privacy & Encapsulation', () => { + it('should create truly private variables', () => { + function createCounter() { + let count = 0 // Private variable! + + return { + increment() { + count++ + return count + }, + decrement() { + count-- + return count + }, + getCount() { + return count + } + } + } + + const counter = createCounter() + + expect(counter.getCount()).toBe(0) + expect(counter.increment()).toBe(1) + expect(counter.increment()).toBe(2) + expect(counter.decrement()).toBe(1) + expect(counter.count).toBe(undefined) // Cannot access directly! + }) + }) + + describe('Function Factories', () => { + it('should create specialized functions', () => { + function createMultiplier(multiplier) { + return function(number) { + return number * multiplier + } + } + + const double = createMultiplier(2) + const triple = createMultiplier(3) + const tenX = createMultiplier(10) + + expect(double(5)).toBe(10) + expect(triple(5)).toBe(15) + expect(tenX(5)).toBe(50) + }) + + it('should create API clients with base URL', () => { + function createApiClient(baseUrl) { + return { + getUrl(endpoint) { + return `${baseUrl}${endpoint}` + } + } + } + + const githubApi = createApiClient('https://api.github.com') + const myApi = createApiClient('https://myapp.com/api') + + expect(githubApi.getUrl('/users')).toBe('https://api.github.com/users') + expect(myApi.getUrl('/users')).toBe('https://myapp.com/api/users') + }) + }) + + describe('Preserving State in Callbacks', () => { + it('should maintain state across multiple calls', () => { + function setupClickCounter() { + let clicks = 0 + + return function handleClick() { + clicks++ + return clicks + } + } + + const handleClick = setupClickCounter() + + expect(handleClick()).toBe(1) + expect(handleClick()).toBe(2) + expect(handleClick()).toBe(3) + }) + }) + + describe('Memoization', () => { + it('should cache expensive computation results', () => { + function createMemoizedFunction(fn) { + const cache = {} + + return function(arg) { + if (arg in cache) { + return { value: cache[arg], cached: true } + } + + const result = fn(arg) + cache[arg] = result + return { value: result, cached: false } + } + } + + function factorial(n) { + if (n <= 1) return 1 + return n * factorial(n - 1) + } + + const memoizedFactorial = createMemoizedFunction(factorial) + + const first = memoizedFactorial(5) + expect(first.value).toBe(120) + expect(first.cached).toBe(false) + + const second = memoizedFactorial(5) + expect(second.value).toBe(120) + expect(second.cached).toBe(true) + }) + }) + }) + + describe('The Classic Closure Trap', () => { + describe('The Problem with var in Loops', () => { + it('should demonstrate the problem', () => { + const funcs = [] + + for (var i = 0; i < 3; i++) { + funcs.push(function() { + return i + }) + } + + // All functions return 3 because they share the same 'i' + expect(funcs[0]()).toBe(3) + expect(funcs[1]()).toBe(3) + expect(funcs[2]()).toBe(3) + }) + }) + + describe('Solution 1: Use let', () => { + it('should create new binding per iteration', () => { + const funcs = [] + + for (let i = 0; i < 3; i++) { + funcs.push(function() { + return i + }) + } + + expect(funcs[0]()).toBe(0) + expect(funcs[1]()).toBe(1) + expect(funcs[2]()).toBe(2) + }) + }) + + describe('Solution 2: IIFE', () => { + it('should capture value in IIFE scope', () => { + const funcs = [] + + for (var i = 0; i < 3; i++) { + (function(j) { + funcs.push(function() { + return j + }) + })(i) + } + + expect(funcs[0]()).toBe(0) + expect(funcs[1]()).toBe(1) + expect(funcs[2]()).toBe(2) + }) + }) + + describe('Solution 3: forEach', () => { + it('should create new scope per iteration', () => { + const funcs = [] + + ;[0, 1, 2].forEach(function(i) { + funcs.push(function() { + return i + }) + }) + + expect(funcs[0]()).toBe(0) + expect(funcs[1]()).toBe(1) + expect(funcs[2]()).toBe(2) + }) + }) + }) + + describe('Closure Memory Considerations', () => { + it('should demonstrate closure keeping references alive', () => { + function createClosure() { + const data = { value: 42 } + + return function() { + return data.value + } + } + + const getClosure = createClosure() + // data is kept alive because getClosure references it + expect(getClosure()).toBe(42) + }) + + it('should demonstrate cleanup with null assignment', () => { + function createHandler() { + let largeData = new Array(100).fill('data') + + const handler = function() { + return largeData.length + } + + return { + handler, + cleanup() { + largeData = null // Allow garbage collection + } + } + } + + const { handler, cleanup } = createHandler() + expect(handler()).toBe(100) + + cleanup() + // Now largeData can be garbage collected + }) + }) + + describe('Practical Closure Patterns', () => { + describe('Private State Pattern', () => { + it('should create objects with private state', () => { + function createWallet(initial) { + let balance = initial + + return { + spend(amount) { + if (amount <= balance) { + balance -= amount + return true + } + return false + }, + getBalance() { + return balance + } + } + } + + const wallet = createWallet(100) + expect(wallet.getBalance()).toBe(100) + expect(wallet.spend(30)).toBe(true) + expect(wallet.getBalance()).toBe(70) + expect(wallet.spend(100)).toBe(false) + expect(wallet.getBalance()).toBe(70) + }) + }) + + describe('Tax Calculator Factory', () => { + it('should create specialized tax calculators', () => { + function createTaxCalculator(rate) { + return (amount) => amount * rate + } + + const calculateVAT = createTaxCalculator(0.20) + const calculateGST = createTaxCalculator(0.10) + + expect(calculateVAT(100)).toBe(20) + expect(calculateGST(100)).toBe(10) + }) + }) + + describe('Logger Factory', () => { + it('should create prefixed loggers', () => { + function setupLogger(prefix) { + return (message) => `[${prefix}] ${message}` + } + + const errorLogger = setupLogger('ERROR') + const infoLogger = setupLogger('INFO') + + expect(errorLogger('Something went wrong')).toBe('[ERROR] Something went wrong') + expect(infoLogger('All good')).toBe('[INFO] All good') + }) + }) + + describe('Memoize Pattern', () => { + it('should cache results with closures', () => { + function memoize(fn) { + const cache = {} + return (arg) => cache[arg] ?? (cache[arg] = fn(arg)) + } + + let callCount = 0 + const expensive = (n) => { + callCount++ + return n * n + } + + const memoizedExpensive = memoize(expensive) + + expect(memoizedExpensive(5)).toBe(25) + expect(callCount).toBe(1) + + expect(memoizedExpensive(5)).toBe(25) + expect(callCount).toBe(1) // Still 1, used cache + + expect(memoizedExpensive(6)).toBe(36) + expect(callCount).toBe(2) + }) + }) + }) + + describe('Test Your Knowledge Examples', () => { + it('should identify all three scope types', () => { + const globalVar = "everywhere" // Global scope + + function example() { + var functionScoped = "function" // Function scope + + if (true) { + let blockScoped = "block" // Block scope + expect(blockScoped).toBe("block") + } + + expect(functionScoped).toBe("function") + } + + example() + expect(globalVar).toBe("everywhere") + }) + + it('should demonstrate closure definition', () => { + function createCounter() { + let count = 0 + + return function() { + count++ + return count + } + } + + const counter = createCounter() + expect(counter()).toBe(1) + expect(counter()).toBe(2) + }) + + it('should show var loop outputs 3,3,3 pattern', () => { + const results = [] + + for (var i = 0; i < 3; i++) { + results.push(() => i) + } + + expect(results[0]()).toBe(3) + expect(results[1]()).toBe(3) + expect(results[2]()).toBe(3) + }) + + it('should show let loop outputs 0,1,2 pattern', () => { + const results = [] + + for (let i = 0; i < 3; i++) { + results.push(() => i) + } + + expect(results[0]()).toBe(0) + expect(results[1]()).toBe(1) + expect(results[2]()).toBe(2) + }) + }) + + describe('Temporal Dead Zone (TDZ)', () => { + it('should throw ReferenceError when accessing let before declaration', () => { + expect(() => { + // Using eval to avoid syntax errors at parse time + eval(` + const before = x + let x = 10 + `) + }).toThrow(ReferenceError) + }) + + it('should throw ReferenceError when accessing const before declaration', () => { + expect(() => { + eval(` + const before = y + const y = 10 + `) + }).toThrow(ReferenceError) + }) + + it('should demonstrate TDZ exists from block start to declaration', () => { + let outsideValue = "outside" + + expect(() => { + eval(` + { + // TDZ starts here for 'name' + const beforeDeclaration = name // ReferenceError + let name = "Alice" + } + `) + }).toThrow(ReferenceError) + }) + + it('should not throw for var due to hoisting', () => { + // var is hoisted with undefined value, so no TDZ + function example() { + const before = message // undefined, not an error + var message = "Hello" + const after = message // "Hello" + return { before, after } + } + + const result = example() + expect(result.before).toBe(undefined) + expect(result.after).toBe("Hello") + }) + + it('should have TDZ in function parameters', () => { + // Default parameters can reference earlier parameters but not later ones + expect(() => { + eval(` + function test(a = b, b = 2) { + return a + b + } + test() + `) + }).toThrow(ReferenceError) + }) + + it('should allow later parameters to reference earlier ones', () => { + function test(a = 1, b = a + 1) { + return a + b + } + + expect(test()).toBe(3) // a=1, b=2 + expect(test(5)).toBe(11) // a=5, b=6 + expect(test(5, 10)).toBe(15) // a=5, b=10 + }) + }) + + describe('Hoisting Comparison: var vs let/const', () => { + it('should demonstrate var hoisting without TDZ', () => { + function example() { + // var is hoisted and initialized to undefined + expect(a).toBe(undefined) + var a = 1 + expect(a).toBe(1) + } + + example() + }) + + it('should demonstrate function declarations are fully hoisted', () => { + // Function can be called before its declaration + expect(hoistedFn()).toBe("I was hoisted!") + + function hoistedFn() { + return "I was hoisted!" + } + }) + + it('should demonstrate function expressions are not hoisted', () => { + expect(() => { + eval(` + notHoisted() + var notHoisted = function() { return "Not hoisted" } + `) + }).toThrow(TypeError) + }) + }) +}) diff --git a/tests/fundamentals/type-coercion/type-coercion.test.js b/tests/fundamentals/type-coercion/type-coercion.test.js new file mode 100644 index 00000000..757561e3 --- /dev/null +++ b/tests/fundamentals/type-coercion/type-coercion.test.js @@ -0,0 +1,635 @@ +import { describe, it, expect } from 'vitest' + +describe('Type Coercion', () => { + describe('Explicit vs Implicit Coercion', () => { + describe('Explicit Coercion', () => { + it('should convert to number explicitly', () => { + expect(Number("42")).toBe(42) + expect(parseInt("42px")).toBe(42) + expect(parseFloat("3.14")).toBe(3.14) + }) + + it('should convert to string explicitly', () => { + expect(String(42)).toBe("42") + expect((123).toString()).toBe("123") + }) + + it('should convert to boolean explicitly', () => { + expect(Boolean(1)).toBe(true) + expect(Boolean(0)).toBe(false) + }) + }) + + describe('Implicit Coercion', () => { + it('should demonstrate implicit string coercion with +', () => { + expect("5" + 3).toBe("53") + expect("5" - 3).toBe(2) + }) + + it('should demonstrate implicit boolean coercion in conditions', () => { + let result = null + if ("hello") { + result = "truthy" + } + expect(result).toBe("truthy") + }) + + it('should demonstrate loose equality coercion', () => { + expect(5 == "5").toBe(true) + }) + }) + }) + + describe('String Conversion', () => { + it('should convert various types to string explicitly', () => { + expect(String(123)).toBe("123") + expect(String(true)).toBe("true") + expect(String(false)).toBe("false") + expect(String(null)).toBe("null") + expect(String(undefined)).toBe("undefined") + }) + + it('should convert to string implicitly with + and empty string', () => { + expect(123 + "").toBe("123") + expect(`Value: ${123}`).toBe("Value: 123") + expect("Hello " + 123).toBe("Hello 123") + }) + + it('should convert arrays to comma-separated strings', () => { + expect([1, 2, 3].toString()).toBe("1,2,3") + expect([1, 2, 3] + "").toBe("1,2,3") + expect([].toString()).toBe("") + }) + + it('should convert objects to [object Object]', () => { + expect({}.toString()).toBe("[object Object]") + expect({} + "").toBe("[object Object]") + }) + + describe('The + Operator Split Personality', () => { + it('should add two numbers', () => { + expect(5 + 3).toBe(8) + }) + + it('should concatenate with any string involved', () => { + expect("5" + 3).toBe("53") + expect(5 + "3").toBe("53") + expect("Hello" + " World").toBe("Hello World") + }) + + it('should evaluate left to right with multiple operands', () => { + expect(1 + 2 + "3").toBe("33") // (1+2)=3, then 3+"3"="33" + expect("1" + 2 + 3).toBe("123") // all become strings left-to-right + }) + }) + + describe('User Input Gotcha', () => { + it('should demonstrate string concatenation instead of addition', () => { + const userInput = "5" + const wrongResult = userInput + 10 + expect(wrongResult).toBe("510") + + const correctResult = Number(userInput) + 10 + expect(correctResult).toBe(15) + }) + }) + }) + + describe('Number Conversion', () => { + it('should convert strings to numbers', () => { + expect(Number("42")).toBe(42) + expect(Number(" 123 ")).toBe(123) // Whitespace trimmed + expect(Number.isNaN(Number("123abc"))).toBe(true) // NaN + expect(Number("")).toBe(0) // Empty string → 0 + expect(Number(" ")).toBe(0) // Whitespace-only → 0 + }) + + it('should convert booleans to numbers', () => { + expect(Number(true)).toBe(1) + expect(Number(false)).toBe(0) + }) + + it('should demonstrate null vs undefined conversion difference', () => { + expect(Number(null)).toBe(0) + expect(Number.isNaN(Number(undefined))).toBe(true) + expect(null + 5).toBe(5) + expect(Number.isNaN(undefined + 5)).toBe(true) + }) + + it('should convert arrays to numbers', () => { + expect(Number([])).toBe(0) // [] → "" → 0 + expect(Number([1])).toBe(1) // [1] → "1" → 1 + expect(Number.isNaN(Number([1, 2]))).toBe(true) // [1,2] → "1,2" → NaN + }) + + it('should return NaN for objects', () => { + expect(Number.isNaN(Number({}))).toBe(true) + }) + + describe('Math Operators Always Convert to Numbers', () => { + it('should convert operands to numbers for -, *, /, %', () => { + expect("6" - "2").toBe(4) + expect("6" * "2").toBe(12) + expect("6" / "2").toBe(3) + expect("10" % "3").toBe(1) + }) + + it('should show why - and + behave differently', () => { + expect("5" + 3).toBe("53") // concatenation + expect("5" - 3).toBe(2) // math + }) + }) + + describe('Unary + Trick', () => { + it('should convert to number with unary +', () => { + expect(+"42").toBe(42) + expect(+true).toBe(1) + expect(+false).toBe(0) + expect(+null).toBe(0) + expect(Number.isNaN(+undefined)).toBe(true) + expect(Number.isNaN(+"hello")).toBe(true) + expect(+"").toBe(0) + }) + }) + }) + + describe('Boolean Conversion', () => { + describe('The 8 Falsy Values', () => { + it('should identify all 8 falsy values', () => { + expect(Boolean(false)).toBe(false) + expect(Boolean(0)).toBe(false) + expect(Boolean(-0)).toBe(false) + expect(Boolean(0n)).toBe(false) + expect(Boolean("")).toBe(false) + expect(Boolean(null)).toBe(false) + expect(Boolean(undefined)).toBe(false) + expect(Boolean(NaN)).toBe(false) + }) + }) + + describe('Truthy Values', () => { + it('should identify truthy values including surprises', () => { + expect(Boolean(true)).toBe(true) + expect(Boolean(1)).toBe(true) + expect(Boolean(-1)).toBe(true) + expect(Boolean("hello")).toBe(true) + expect(Boolean("0")).toBe(true) // Non-empty string! + expect(Boolean("false")).toBe(true) // Non-empty string! + expect(Boolean([])).toBe(true) // Empty array! + expect(Boolean({})).toBe(true) // Empty object! + expect(Boolean(function(){})).toBe(true) + expect(Boolean(new Date())).toBe(true) + expect(Boolean(Infinity)).toBe(true) + expect(Boolean(-Infinity)).toBe(true) + }) + }) + + describe('Common Gotchas', () => { + it('should demonstrate empty array checking', () => { + const arr = [] + expect(Boolean(arr)).toBe(true) // Array itself is truthy + expect(arr.length > 0).toBe(false) // Check length instead + }) + }) + + describe('Logical Operators Return Original Values', () => { + it('should return first truthy value with ||', () => { + expect("hello" || "world").toBe("hello") + expect("" || "world").toBe("world") + expect("" || 0 || null || "yes").toBe("yes") + }) + + it('should return first falsy value with &&', () => { + expect("hello" && "world").toBe("world") + expect("" && "world").toBe("") + expect(1 && 2 && 3).toBe(3) + }) + + it('should use || for defaults', () => { + const userInput = "" + const name = userInput || "Anonymous" + expect(name).toBe("Anonymous") + }) + }) + }) + + describe('Object to Primitive Conversion', () => { + describe('Built-in Object Conversion', () => { + it('should convert arrays via toString', () => { + expect([1, 2, 3].toString()).toBe("1,2,3") + expect([1, 2, 3] + "").toBe("1,2,3") + expect(Number.isNaN([1, 2, 3] - 0)).toBe(true) // "1,2,3" → NaN + + expect([].toString()).toBe("") + expect([] + "").toBe("") + expect([] - 0).toBe(0) // "" → 0 + + expect([1].toString()).toBe("1") + expect([1] - 0).toBe(1) + }) + + it('should convert plain objects to [object Object]', () => { + expect({}.toString()).toBe("[object Object]") + expect({} + "").toBe("[object Object]") + }) + }) + + describe('Custom valueOf and toString', () => { + it('should use valueOf for number conversion', () => { + const price = { + amount: 99.99, + currency: "USD", + valueOf() { + return this.amount + }, + toString() { + return `${this.currency} ${this.amount}` + } + } + + expect(price - 0).toBe(99.99) + expect(price * 2).toBe(199.98) + expect(+price).toBe(99.99) + }) + + it('should use toString for string conversion', () => { + const price = { + amount: 99.99, + currency: "USD", + valueOf() { + return this.amount + }, + toString() { + return `${this.currency} ${this.amount}` + } + } + + expect(String(price)).toBe("USD 99.99") + expect(`Price: ${price}`).toBe("Price: USD 99.99") + }) + }) + + describe('Symbol.toPrimitive', () => { + it('should use Symbol.toPrimitive for conversion hints', () => { + const obj = { + [Symbol.toPrimitive](hint) { + if (hint === "number") { + return 42 + } + if (hint === "string") { + return "forty-two" + } + return "default value" + } + } + + expect(+obj).toBe(42) // hint: "number" + expect(`${obj}`).toBe("forty-two") // hint: "string" + expect(obj + "").toBe("default value") // hint: "default" + }) + }) + }) + + describe('The == Algorithm', () => { + describe('Same Type Comparison', () => { + it('should compare directly when same type', () => { + expect(5 == 5).toBe(true) + expect("hello" == "hello").toBe(true) + }) + }) + + describe('null and undefined', () => { + it('should return true for null == undefined', () => { + expect(null == undefined).toBe(true) + expect(undefined == null).toBe(true) + }) + + it('should return false for null/undefined vs other values', () => { + expect(null == 0).toBe(false) + expect(null == "").toBe(false) + expect(undefined == 0).toBe(false) + expect(undefined == "").toBe(false) + }) + }) + + describe('Number and String', () => { + it('should convert string to number', () => { + expect(5 == "5").toBe(true) + expect(0 == "").toBe(true) + expect(42 == "42").toBe(true) + }) + }) + + describe('Boolean Conversion', () => { + it('should convert boolean to number first', () => { + expect(true == 1).toBe(true) + expect(false == 0).toBe(true) + expect(true == "1").toBe(true) + }) + + it('should demonstrate confusing boolean comparisons', () => { + expect(true == "true").toBe(false) // true → 1, "true" → NaN + expect(false == "false").toBe(false) // false → 0, "false" → NaN + expect(true == 2).toBe(false) // true → 1, 1 ≠ 2 + }) + }) + + describe('Object to Primitive', () => { + it('should convert object to primitive', () => { + expect([1] == 1).toBe(true) // [1] → "1" → 1 + expect([""] == 0).toBe(true) // [""] → "" → 0 + }) + }) + + describe('Step-by-Step Examples', () => { + it('should trace "5" == 5', () => { + // String vs Number → convert string to number + // 5 == 5 → true + expect("5" == 5).toBe(true) + }) + + it('should trace true == "1"', () => { + // Boolean → number: 1 == "1" + // Number vs String → 1 == 1 + // true + expect(true == "1").toBe(true) + }) + + it('should trace [] == false', () => { + // Boolean → number: [] == 0 + // Object → primitive: "" == 0 + // String → number: 0 == 0 + // true + expect([] == false).toBe(true) + }) + + it('should trace [] == ![]', () => { + // First, evaluate ![] → false (arrays are truthy) + // [] == false + // Boolean → number: [] == 0 + // Object → primitive: "" == 0 + // String → number: 0 == 0 + // true! + expect([] == ![]).toBe(true) + }) + }) + }) + + describe('JavaScript WAT Moments', () => { + describe('+ Operator Split Personality', () => { + it('should show string vs number behavior', () => { + expect("5" + 3).toBe("53") + expect("5" - 3).toBe(2) + }) + }) + + describe('Empty Array Weirdness', () => { + it('should demonstrate [] + [] behavior', () => { + expect([] + []).toBe("") // Both → "", then "" + "" = "" + }) + + it('should demonstrate [] + {} behavior', () => { + expect([] + {}).toBe("[object Object]") + }) + }) + + describe('Boolean Math', () => { + it('should add booleans as numbers', () => { + expect(true + true).toBe(2) + expect(true + false).toBe(1) + expect(true - true).toBe(0) + }) + }) + + describe('The Infamous [] == ![]', () => { + it('should return true for [] == ![]', () => { + const emptyArr = [] + const negatedArr = ![] + expect(emptyArr == negatedArr).toBe(true) + expect(emptyArr === negatedArr).toBe(false) + }) + }) + + describe('"foo" + + "bar"', () => { + it('should return "fooNaN"', () => { + // +"bar" is evaluated first → NaN + // "foo" + NaN → "fooNaN" + expect("foo" + + "bar").toBe("fooNaN") + }) + }) + + describe('NaN Equality', () => { + it('should never equal itself', () => { + expect(NaN === NaN).toBe(false) + expect(NaN == NaN).toBe(false) + }) + + it('should use Number.isNaN to check', () => { + expect(Number.isNaN(NaN)).toBe(true) + expect(isNaN(NaN)).toBe(true) + expect(isNaN("hello")).toBe(true) // Quirk: converts first + expect(Number.isNaN("hello")).toBe(false) // Correct + }) + }) + + describe('typeof Quirks', () => { + it('should demonstrate typeof oddities', () => { + expect(typeof NaN).toBe("number") // "Not a Number" is a number + expect(typeof null).toBe("object") // Historical bug + expect(typeof []).toBe("object") // Arrays are objects + expect(typeof function(){}).toBe("function") // Special case + }) + }) + + describe('Adding Arrays', () => { + it('should concatenate arrays as strings', () => { + expect([1, 2] + [3, 4]).toBe("1,23,4") + // [1, 2] → "1,2" + // [3, 4] → "3,4" + // "1,2" + "3,4" → "1,23,4" + }) + + it('should use spread or concat to combine arrays', () => { + expect([...[1, 2], ...[3, 4]]).toEqual([1, 2, 3, 4]) + expect([1, 2].concat([3, 4])).toEqual([1, 2, 3, 4]) + }) + }) + }) + + describe('Best Practices', () => { + describe('Use === instead of ==', () => { + it('should show predictable strict equality', () => { + expect(5 === "5").toBe(false) // No coercion + expect(5 == "5").toBe(true) // Coerced + }) + }) + + describe('When == IS Useful', () => { + it('should check for null or undefined in one shot', () => { + function checkNullish(value) { + return value == null + } + + expect(checkNullish(null)).toBe(true) + expect(checkNullish(undefined)).toBe(true) + expect(checkNullish(0)).toBe(false) + expect(checkNullish("")).toBe(false) + }) + }) + + describe('Be Explicit with Conversions', () => { + it('should convert explicitly for clarity', () => { + const str = "42" + + // Quick string conversion + expect(str + "").toBe("42") + expect(String(42)).toBe("42") + + // Quick number conversion + expect(+str).toBe(42) + expect(Number(str)).toBe(42) + }) + }) + }) + + describe('Test Your Knowledge Examples', () => { + it('should return "53" for "5" + 3', () => { + expect("5" + 3).toBe("53") + }) + + it('should return "1hello" for true + false + "hello"', () => { + // true + false = 1 + 0 = 1 + // 1 + "hello" = "1hello" + expect(true + false + "hello").toBe("1hello") + }) + }) + + describe('Modulo Operator with Strings', () => { + it('should coerce strings to numbers for modulo', () => { + expect("6" % 4).toBe(2) + expect("10" % "3").toBe(1) + expect(17 % "5").toBe(2) + }) + + it('should return NaN for non-numeric strings', () => { + expect(Number.isNaN("hello" % 2)).toBe(true) + expect(Number.isNaN(10 % "abc")).toBe(true) + }) + }) + + describe('Comparison Operators with Coercion', () => { + it('should coerce strings to numbers in comparisons', () => { + expect("10" > 5).toBe(true) + expect("10" < 5).toBe(false) + expect("10" >= 10).toBe(true) + expect("10" <= 10).toBe(true) + }) + + it('should compare strings lexicographically when both are strings', () => { + // String comparison (lexicographic, not numeric) + expect("10" > "9").toBe(false) // "1" < "9" in char codes + expect("2" > "10").toBe(true) // "2" > "1" in char codes + }) + + it('should coerce null and undefined in comparisons', () => { + expect(null >= 0).toBe(true) // null coerces to 0 + expect(null > 0).toBe(false) + expect(null == 0).toBe(false) // Special case! + + // undefined always returns false in comparisons + expect(undefined > 0).toBe(false) + expect(undefined < 0).toBe(false) + expect(undefined >= 0).toBe(false) + }) + }) + + describe('Double Negation (!!)', () => { + it('should convert values to boolean with !!', () => { + // Truthy values + expect(!!"hello").toBe(true) + expect(!!1).toBe(true) + expect(!!{}).toBe(true) + expect(!![]).toBe(true) + expect(!!-1).toBe(true) + + // Falsy values + expect(!!"").toBe(false) + expect(!!0).toBe(false) + expect(!!null).toBe(false) + expect(!!undefined).toBe(false) + expect(!!NaN).toBe(false) + }) + + it('should be equivalent to Boolean()', () => { + const values = ["hello", "", 0, 1, null, undefined, {}, []] + + values.forEach(value => { + expect(!!value).toBe(Boolean(value)) + }) + }) + }) + + describe('Date Coercion', () => { + it('should coerce Date to number (timestamp) with + operator', () => { + const date = new Date("2025-01-01T00:00:00.000Z") + const timestamp = +date + + expect(typeof timestamp).toBe("number") + expect(timestamp).toBe(date.getTime()) + }) + + it('should coerce Date to string with String()', () => { + const date = new Date("2025-06-15T12:00:00.000Z") + const str = String(date) + + expect(typeof str).toBe("string") + // Use a mid-year date to avoid timezone edge cases + expect(str).toContain("2025") + }) + + it('should prefer string coercion with + operator and string', () => { + const date = new Date("2025-06-15T12:00:00.000Z") + const result = "Date: " + date + + expect(typeof result).toBe("string") + expect(result).toContain("Date:") + expect(result).toContain("2025") + }) + + it('should use valueOf for numeric context', () => { + const date = new Date("2025-01-01T00:00:00.000Z") + + // In numeric context, Date uses valueOf (returns timestamp) + expect(date - 0).toBe(date.getTime()) + expect(date * 1).toBe(date.getTime()) + }) + }) + + describe('Implicit Boolean Contexts', () => { + it('should coerce to boolean in if statements', () => { + let result = "" + + if ("hello") result += "truthy string " + if (0) result += "zero " + if ([]) result += "empty array " + if ({}) result += "empty object " + + expect(result).toBe("truthy string empty array empty object ") + }) + + it('should coerce to boolean in ternary operator', () => { + expect("hello" ? "yes" : "no").toBe("yes") + expect("" ? "yes" : "no").toBe("no") + expect(0 ? "yes" : "no").toBe("no") + expect(1 ? "yes" : "no").toBe("yes") + }) + + it('should coerce to boolean in logical NOT', () => { + expect(!0).toBe(true) + expect(!"").toBe(true) + expect(!null).toBe(true) + expect(!"hello").toBe(false) + expect(!1).toBe(false) + }) + }) +}) diff --git a/tests/fundamentals/value-reference-types/value-reference-types.test.js b/tests/fundamentals/value-reference-types/value-reference-types.test.js new file mode 100644 index 00000000..6ab7fc56 --- /dev/null +++ b/tests/fundamentals/value-reference-types/value-reference-types.test.js @@ -0,0 +1,765 @@ +import { describe, it, expect } from 'vitest' + +describe('Value Types and Reference Types', () => { + describe('Copying Primitives', () => { + it('should create independent copies when copying primitives', () => { + let a = 10 + let b = a // b gets a COPY of the value 10 + + b = 20 // changing b has NO effect on a + + expect(a).toBe(10) // unchanged! + expect(b).toBe(20) + }) + + it('should demonstrate string variables are independent copies', () => { + let name = "Alice" + let age = 25 + let user = { name: "Alice" } // Reference on stack, object on heap + let scores = [95, 87, 92] // Reference on stack, array on heap + + expect(name).toBe("Alice") + expect(age).toBe(25) + expect(user).toEqual({ name: "Alice" }) + expect(scores).toEqual([95, 87, 92]) + }) + }) + + describe('Copying Objects', () => { + it('should share reference when copying objects', () => { + let obj1 = { name: "Alice" } + let obj2 = obj1 // obj2 gets a COPY of the REFERENCE + + obj2.name = "Bob" // modifies the SAME object! + + expect(obj1.name).toBe("Bob") // changed! + expect(obj2.name).toBe("Bob") + }) + + it('should share reference when copying arrays', () => { + let arr1 = [1, 2, 3] + let arr2 = arr1 // arr2 points to the SAME array + + arr2.push(4) // modifies the shared array + + expect(arr1).toEqual([1, 2, 3, 4]) // changed! + expect(arr2).toEqual([1, 2, 3, 4]) + }) + }) + + describe('Comparison Behavior', () => { + describe('Primitives: Compared by Value', () => { + it('should return true for equal primitive values', () => { + let a = "hello" + let b = "hello" + expect(a === b).toBe(true) + + let x = 42 + let y = 42 + expect(x === y).toBe(true) + }) + }) + + describe('Objects: Compared by Reference', () => { + it('should return false for different objects with same content', () => { + let obj1 = { name: "Alice" } + let obj2 = { name: "Alice" } + expect(obj1 === obj2).toBe(false) // different objects! + }) + + it('should return true for same reference', () => { + let obj1 = { name: "Alice" } + let obj3 = obj1 + expect(obj1 === obj3).toBe(true) // same reference + }) + + it('should return false for empty objects/arrays compared', () => { + // These tests intentionally demonstrate that objects compare by reference + const emptyObj1 = {} + const emptyObj2 = {} + expect(emptyObj1 === emptyObj2).toBe(false) + + const emptyArr1 = [] + const emptyArr2 = [] + expect(emptyArr1 === emptyArr2).toBe(false) + + const arr1 = [1, 2] + const arr2 = [1, 2] + expect(arr1 === arr2).toBe(false) + }) + }) + + describe('Comparing Objects by Content', () => { + it('should use JSON.stringify for simple comparison', () => { + let obj1 = { name: "Alice" } + let obj2 = { name: "Alice" } + expect(JSON.stringify(obj1) === JSON.stringify(obj2)).toBe(true) + }) + + it('should compare arrays of primitives with every()', () => { + let arr1 = [1, 2, 3] + let arr2 = [1, 2, 3] + expect(arr1.length === arr2.length && arr1.every((v, i) => v === arr2[i])).toBe(true) + }) + }) + }) + + describe('Functions and Parameters', () => { + describe('Passing Primitives', () => { + it('should not modify original when passing primitive to function', () => { + function double(num) { + num = num * 2 + return num + } + + let x = 10 + let result = double(x) + + expect(x).toBe(10) // unchanged! + expect(result).toBe(20) + }) + }) + + describe('Passing Objects', () => { + it('should modify original object when mutating through function parameter', () => { + function rename(person) { + person.name = "Bob" + } + + let user = { name: "Alice" } + rename(user) + + expect(user.name).toBe("Bob") // changed! + }) + + it('should not modify original when reassigning parameter', () => { + function replace(person) { + person = { name: "Charlie" } // creates NEW local object + } + + let user = { name: "Alice" } + replace(user) + + expect(user.name).toBe("Alice") // unchanged! + }) + }) + }) + + describe('Mutation vs Reassignment', () => { + describe('Mutation', () => { + it('should modify array with mutating methods', () => { + const arr = [1, 2, 3] + + arr.push(4) + expect(arr).toEqual([1, 2, 3, 4]) + + arr[0] = 99 + expect(arr).toEqual([99, 2, 3, 4]) + + arr.pop() + expect(arr).toEqual([99, 2, 3]) + }) + + it('should modify object properties', () => { + const obj = { name: "Alice" } + + obj.name = "Bob" + expect(obj.name).toBe("Bob") + + obj.age = 25 + expect(obj.age).toBe(25) + + delete obj.age + expect(obj.age).toBe(undefined) + }) + }) + + describe('Reassignment', () => { + it('should point to new value after reassignment', () => { + let arr = [1, 2, 3] + arr = [4, 5, 6] + expect(arr).toEqual([4, 5, 6]) + + let obj = { name: "Alice" } + obj = { name: "Bob" } + expect(obj).toEqual({ name: "Bob" }) + }) + }) + + describe('const with Objects', () => { + it('should allow mutations on const objects', () => { + const arr = [1, 2, 3] + + arr.push(4) + expect(arr).toEqual([1, 2, 3, 4]) + + arr[0] = 99 + expect(arr).toEqual([99, 2, 3, 4]) + }) + + it('should allow mutations on const object properties', () => { + const obj = { name: "Alice" } + + obj.name = "Bob" + expect(obj.name).toBe("Bob") + + obj.age = 25 + expect(obj.age).toBe(25) + }) + + it('should throw TypeError when reassigning const', () => { + expect(() => { + eval('const x = 1; x = 2') // Using eval to test const reassignment + }).toThrow() + }) + }) + }) + + describe('Object.freeze()', () => { + it('should throw TypeError when modifying frozen object in strict mode', () => { + const user = Object.freeze({ name: "Alice", age: 25 }) + + // In strict mode (which Vitest uses), modifications throw TypeError + expect(() => { user.name = "Bob" }).toThrow(TypeError) + expect(() => { user.email = "a@b.com" }).toThrow(TypeError) + expect(() => { delete user.age }).toThrow(TypeError) + + expect(user).toEqual({ name: "Alice", age: 25 }) // unchanged! + }) + + it('should check if object is frozen', () => { + const frozen = Object.freeze({ a: 1 }) + const normal = { a: 1 } + + expect(Object.isFrozen(frozen)).toBe(true) + expect(Object.isFrozen(normal)).toBe(false) + }) + + it('should only freeze shallow - nested objects can still be modified', () => { + const user = Object.freeze({ + name: "Alice", + address: { city: "NYC" } + }) + + // In strict mode, modifying frozen property throws TypeError + expect(() => { user.name = "Bob" }).toThrow(TypeError) + // But nested object is not frozen, so this works + user.address.city = "LA" + + expect(user.name).toBe("Alice") // unchanged + expect(user.address.city).toBe("LA") // changed! + }) + }) + + describe('Deep Freeze', () => { + it('should freeze nested objects with deep freeze function', () => { + function deepFreeze(obj, seen = new WeakSet()) { + // Prevent infinite loops from circular references + if (seen.has(obj)) return obj + seen.add(obj) + + const propNames = Reflect.ownKeys(obj) + + for (const name of propNames) { + const value = obj[name] + if (value && typeof value === "object") { + deepFreeze(value, seen) + } + } + + return Object.freeze(obj) + } + + const user = deepFreeze({ + name: "Alice", + address: { city: "NYC" } + }) + + // In strict mode, this throws TypeError since nested object is now frozen + expect(() => { user.address.city = "LA" }).toThrow(TypeError) + expect(user.address.city).toBe("NYC") // Now blocked! + }) + + it('should handle circular references without infinite loop', () => { + function deepFreeze(obj, seen = new WeakSet()) { + if (seen.has(obj)) return obj + seen.add(obj) + + const propNames = Reflect.ownKeys(obj) + + for (const name of propNames) { + const value = obj[name] + if (value && typeof value === "object") { + deepFreeze(value, seen) + } + } + + return Object.freeze(obj) + } + + // Create object with circular reference + const obj = { name: "test" } + obj.self = obj // Circular reference + + // Should not throw or hang - handles circular reference + const frozen = deepFreeze(obj) + + expect(Object.isFrozen(frozen)).toBe(true) + expect(frozen.self).toBe(frozen) // Circular reference preserved + expect(() => { frozen.name = "changed" }).toThrow(TypeError) + }) + }) + + describe('Object.seal() and Object.preventExtensions()', () => { + it('should allow value changes but prevent add/delete with seal()', () => { + const sealed = Object.seal({ name: "Alice" }) + + sealed.name = "Bob" + expect(sealed.name).toBe("Bob") // Works! + + // In strict mode, these throw TypeError instead of failing silently + expect(() => { sealed.age = 25 }).toThrow(TypeError) + expect(sealed.age).toBe(undefined) + + expect(() => { delete sealed.name }).toThrow(TypeError) + expect(sealed.name).toBe("Bob") + }) + + it('should allow change/delete but prevent add with preventExtensions()', () => { + const noExtend = Object.preventExtensions({ name: "Alice" }) + + noExtend.name = "Bob" + expect(noExtend.name).toBe("Bob") // Works! + + delete noExtend.name + expect(noExtend.name).toBe(undefined) // Works! + + // In strict mode, adding properties throws TypeError + expect(() => { noExtend.age = 25 }).toThrow(TypeError) + expect(noExtend.age).toBe(undefined) + }) + }) + + describe('Shallow Copy', () => { + it('should create shallow copy with spread operator', () => { + const original = { + name: "Alice", + scores: [95, 87, 92], + address: { city: "NYC" } + } + + const copy1 = { ...original } + + expect(copy1.name).toBe("Alice") + expect(copy1).not.toBe(original) // Different objects + }) + + it('should create shallow copy with Object.assign', () => { + const original = { name: "Alice" } + const copy2 = Object.assign({}, original) + + expect(copy2.name).toBe("Alice") + expect(copy2).not.toBe(original) + }) + + it('should share nested objects in shallow copy', () => { + const original = { + name: "Alice", + address: { city: "NYC" } + } + + const shallow = { ...original } + + // Top-level changes are independent + shallow.name = "Bob" + expect(original.name).toBe("Alice") + + // But nested objects are SHARED + shallow.address.city = "LA" + expect(original.address.city).toBe("LA") // Original changed! + }) + + it('should create shallow copy of arrays', () => { + const originalArray = [1, 2, 3] + + const arrCopy1 = [...originalArray] + const arrCopy2 = originalArray.slice() + const arrCopy3 = Array.from(originalArray) + + expect(arrCopy1).toEqual([1, 2, 3]) + expect(arrCopy2).toEqual([1, 2, 3]) + expect(arrCopy3).toEqual([1, 2, 3]) + + expect(arrCopy1).not.toBe(originalArray) + expect(arrCopy2).not.toBe(originalArray) + expect(arrCopy3).not.toBe(originalArray) + }) + }) + + describe('Deep Copy', () => { + it('should create deep copy with structuredClone', () => { + const original = { + name: "Alice", + scores: [95, 87, 92], + address: { city: "NYC" }, + date: new Date() + } + + const deep = structuredClone(original) + + // Everything is independent + deep.address.city = "LA" + expect(original.address.city).toBe("NYC") // unchanged! + + deep.scores.push(100) + expect(original.scores).toEqual([95, 87, 92]) // unchanged! + }) + + it('should create deep copy with JSON trick (with limitations)', () => { + const original = { + name: "Alice", + address: { city: "NYC" } + } + + const deep = JSON.parse(JSON.stringify(original)) + + deep.address.city = "LA" + expect(original.address.city).toBe("NYC") // unchanged! + }) + + it('should demonstrate JSON trick limitations', () => { + const obj = { + fn: () => {}, + date: new Date('2025-01-01'), + undef: undefined, + set: new Set([1, 2]) + } + + const clone = JSON.parse(JSON.stringify(obj)) + + expect(clone.fn).toBe(undefined) // Functions lost + expect(typeof clone.date).toBe('string') // Date becomes string + expect(clone.undef).toBe(undefined) // Property removed + expect(clone.set).toEqual({}) // Set becomes empty object + }) + }) + + describe('Array Methods: Mutating vs Non-Mutating', () => { + describe('Mutating Methods', () => { + it('should mutate array with push, pop, shift, unshift', () => { + const arr = [1, 2, 3] + + arr.push(4) + expect(arr).toEqual([1, 2, 3, 4]) + + arr.pop() + expect(arr).toEqual([1, 2, 3]) + + arr.shift() + expect(arr).toEqual([2, 3]) + + arr.unshift(1) + expect(arr).toEqual([1, 2, 3]) + }) + + it('should mutate array with sort and reverse', () => { + const nums = [3, 1, 2] + nums.sort() + expect(nums).toEqual([1, 2, 3]) // Original mutated! + + nums.reverse() + expect(nums).toEqual([3, 2, 1]) // Original mutated! + }) + + it('should mutate array with splice', () => { + const arr = [1, 2, 3, 4, 5] + arr.splice(2, 1) // Remove 1 element at index 2 + expect(arr).toEqual([1, 2, 4, 5]) + }) + }) + + describe('Non-Mutating Methods', () => { + it('should not mutate with map, filter, slice, concat', () => { + const original = [1, 2, 3] + + const mapped = original.map(x => x * 2) + expect(original).toEqual([1, 2, 3]) + expect(mapped).toEqual([2, 4, 6]) + + const filtered = original.filter(x => x > 1) + expect(original).toEqual([1, 2, 3]) + expect(filtered).toEqual([2, 3]) + + const sliced = original.slice(1) + expect(original).toEqual([1, 2, 3]) + expect(sliced).toEqual([2, 3]) + + const concatenated = original.concat([4, 5]) + expect(original).toEqual([1, 2, 3]) + expect(concatenated).toEqual([1, 2, 3, 4, 5]) + }) + + it('should use toSorted and toReversed for non-mutating sort/reverse (ES2023)', () => { + const nums = [3, 1, 2] + + const sorted = nums.toSorted() + expect(nums).toEqual([3, 1, 2]) // Original unchanged + expect(sorted).toEqual([1, 2, 3]) + + const reversed = nums.toReversed() + expect(nums).toEqual([3, 1, 2]) // Original unchanged + expect(reversed).toEqual([2, 1, 3]) + }) + }) + + describe('Safe Sorting Pattern', () => { + it('should copy array before sorting to avoid mutation', () => { + const nums = [3, 1, 2] + const sorted = [...nums].sort() + + expect(nums).toEqual([3, 1, 2]) // Original unchanged + expect(sorted).toEqual([1, 2, 3]) + }) + }) + }) + + describe('Common Pitfalls', () => { + it('should demonstrate accidental array mutation in function', () => { + function processUsers(users) { + const copy = [...users] + copy.push({ name: "New User" }) + return copy + } + + const myUsers = [{ name: "Alice" }] + const result = processUsers(myUsers) + + expect(myUsers).toEqual([{ name: "Alice" }]) // Original unchanged + expect(result).toEqual([{ name: "Alice" }, { name: "New User" }]) + }) + + it('should demonstrate backup pattern failure', () => { + const original = [1, 2, 3] + const notABackup = original // NOT a backup! + + original.push(4) + expect(notABackup).toEqual([1, 2, 3, 4]) // "backup" changed! + + // Correct backup + const original2 = [1, 2, 3] + const backup = [...original2] + + original2.push(4) + expect(backup).toEqual([1, 2, 3]) // Real backup unchanged + }) + + it('should demonstrate deep equality comparison', () => { + function deepEqual(a, b) { + return JSON.stringify(a) === JSON.stringify(b) + } + + const obj1 = { name: "Alice", age: 25 } + const obj2 = { name: "Alice", age: 25 } + + expect(obj1 === obj2).toBe(false) + expect(deepEqual(obj1, obj2)).toBe(true) + }) + }) + + describe('Best Practices: Immutable Patterns', () => { + it('should create new object instead of mutating', () => { + const user = { name: "Alice", age: 25 } + + // Instead of: user.name = "Bob" + const updatedUser = { ...user, name: "Bob" } + + expect(user.name).toBe("Alice") // Original unchanged + expect(updatedUser.name).toBe("Bob") + }) + + it('should use non-mutating array methods', () => { + const numbers = [3, 1, 2] + + // Instead of: numbers.sort() + const sorted = [...numbers].sort((a, b) => a - b) + + expect(numbers).toEqual([3, 1, 2]) // Original unchanged + expect(sorted).toEqual([1, 2, 3]) + }) + }) + + describe('structuredClone with Special Types', () => { + it('should deep clone objects with Map', () => { + const original = { + name: "Alice", + data: new Map([["key1", "value1"], ["key2", "value2"]]) + } + + const clone = structuredClone(original) + + // Modify the clone's Map + clone.data.set("key1", "modified") + clone.data.set("key3", "new value") + + // Original should be unchanged + expect(original.data.get("key1")).toBe("value1") + expect(original.data.has("key3")).toBe(false) + expect(clone.data.get("key1")).toBe("modified") + }) + + it('should deep clone objects with Set', () => { + const original = { + name: "Alice", + tags: new Set([1, 2, 3]) + } + + const clone = structuredClone(original) + + // Modify the clone's Set + clone.tags.add(4) + clone.tags.delete(1) + + // Original should be unchanged + expect(original.tags.has(1)).toBe(true) + expect(original.tags.has(4)).toBe(false) + expect(clone.tags.has(1)).toBe(false) + expect(clone.tags.has(4)).toBe(true) + }) + + it('should deep clone objects with Date', () => { + const original = { + name: "Event", + date: new Date("2025-01-01") + } + + const clone = structuredClone(original) + + expect(clone.date instanceof Date).toBe(true) + expect(clone.date.getTime()).toBe(original.date.getTime()) + expect(clone.date).not.toBe(original.date) // Different reference + }) + }) + + describe('Shared Default Object Reference Pitfall', () => { + it('should demonstrate shared default array problem', () => { + const defaultList = [] + + function addItem(item, list = defaultList) { + list.push(item) + return list + } + + const result1 = addItem("a") + const result2 = addItem("b") + + // Both calls modified the same defaultList! + expect(result1).toEqual(["a", "b"]) + expect(result2).toEqual(["a", "b"]) + expect(result1).toBe(result2) // Same reference! + }) + + it('should fix shared default with new array creation', () => { + function addItem(item, list = []) { + list.push(item) + return list + } + + const result1 = addItem("a") + const result2 = addItem("b") + + // Each call gets its own array + expect(result1).toEqual(["a"]) + expect(result2).toEqual(["b"]) + expect(result1).not.toBe(result2) + }) + }) + + describe('WeakMap vs Map Memory Behavior', () => { + it('should demonstrate Map holds strong references', () => { + const cache = new Map() + let user = { id: 1, name: "Alice" } + + cache.set(user.id, user) + + // Even if we clear user, the Map still holds the reference + const cachedUser = cache.get(1) + expect(cachedUser.name).toBe("Alice") + }) + + it('should demonstrate WeakMap allows garbage collection', () => { + const cache = new WeakMap() + let user = { id: 1, name: "Alice" } + + cache.set(user, { computed: "expensive data" }) + + // WeakMap uses the object itself as key + expect(cache.get(user)).toEqual({ computed: "expensive data" }) + + // WeakMap keys must be objects + expect(() => cache.set("string-key", "value")).toThrow(TypeError) + }) + + it('should show WeakMap cannot be iterated', () => { + const weakMap = new WeakMap() + const obj = { id: 1 } + weakMap.set(obj, "value") + + // WeakMap has no size property + expect(weakMap.size).toBe(undefined) + + // WeakMap is not iterable + expect(typeof weakMap[Symbol.iterator]).toBe("undefined") + }) + }) + + describe('Clone Function Parameters Pattern', () => { + it('should clone parameters before modification', () => { + function processData(data) { + // Clone to avoid modifying original + const copy = structuredClone(data) + copy.processed = true + copy.items.push("new item") + return copy + } + + const original = { + name: "data", + items: ["item1", "item2"] + } + + const result = processData(original) + + // Original is unchanged + expect(original.processed).toBe(undefined) + expect(original.items).toEqual(["item1", "item2"]) + + // Result has modifications + expect(result.processed).toBe(true) + expect(result.items).toEqual(["item1", "item2", "new item"]) + }) + }) + + describe('let with Object.freeze()', () => { + it('should allow reassignment of let variable holding frozen object', () => { + let obj = Object.freeze({ a: 1 }) + + // Cannot modify the frozen object + expect(() => { obj.a = 2 }).toThrow(TypeError) + + // But CAN reassign the variable to a new object + obj = { a: 2 } + expect(obj.a).toBe(2) + }) + + it('should demonstrate const + freeze for true immutability', () => { + const obj = Object.freeze({ a: 1 }) + + // Cannot modify the frozen object + expect(() => { obj.a = 2 }).toThrow(TypeError) + + // Cannot reassign const + // obj = { a: 2 } // Would throw TypeError + expect(obj.a).toBe(1) + }) + }) +}) diff --git a/tests/object-oriented/factories-classes/factories-classes.test.js b/tests/object-oriented/factories-classes/factories-classes.test.js new file mode 100644 index 00000000..0cd99538 --- /dev/null +++ b/tests/object-oriented/factories-classes/factories-classes.test.js @@ -0,0 +1,1463 @@ +import { describe, it, expect } from 'vitest' + +describe('Factories and Classes', () => { + // =========================================== + // Opening Example: Factory vs Class + // =========================================== + + describe('Opening Example: Factory vs Class', () => { + it('should create objects with factory function', () => { + function createPlayer(name) { + return { + name, + health: 100, + attack() { + return `${this.name} attacks!` + } + } + } + + const player = createPlayer('Alice') + + expect(player.name).toBe('Alice') + expect(player.health).toBe(100) + expect(player.attack()).toBe('Alice attacks!') + }) + + it('should create objects with class', () => { + class Enemy { + constructor(name) { + this.name = name + this.health = 100 + } + + attack() { + return `${this.name} attacks!` + } + } + + const enemy = new Enemy('Goblin') + + expect(enemy.name).toBe('Goblin') + expect(enemy.health).toBe(100) + expect(enemy.attack()).toBe('Goblin attacks!') + }) + + it('should show both patterns produce similar results', () => { + function createPlayer(name) { + return { + name, + health: 100, + attack() { + return `${this.name} attacks!` + } + } + } + + class Enemy { + constructor(name) { + this.name = name + this.health = 100 + } + + attack() { + return `${this.name} attacks!` + } + } + + const player = createPlayer('Alice') + const enemy = new Enemy('Goblin') + + // Both have same structure + expect(player.name).toBe('Alice') + expect(enemy.name).toBe('Goblin') + expect(player.health).toBe(enemy.health) + + // Both attack methods work + expect(player.attack()).toBe('Alice attacks!') + expect(enemy.attack()).toBe('Goblin attacks!') + }) + }) + + // =========================================== + // Part 1: The Problem — Manual Object Creation + // =========================================== + + describe('Part 1: The Problem — Manual Object Creation', () => { + it('should show manual object creation is repetitive', () => { + const player1 = { + name: 'Alice', + health: 100, + attack() { + return `${this.name} attacks!` + } + } + + const player2 = { + name: 'Bob', + health: 100, + attack() { + return `${this.name} attacks!` + } + } + + expect(player1.attack()).toBe('Alice attacks!') + expect(player2.attack()).toBe('Bob attacks!') + + // Each object has its own copy of the method + expect(player1.attack).not.toBe(player2.attack) + }) + }) + + // =========================================== + // Part 2: Factory Functions + // =========================================== + + describe('Part 2: Factory Functions', () => { + describe('Basic Factory Function', () => { + it('should create objects with a factory function', () => { + function createPlayer(name) { + return { + name, + health: 100, + level: 1, + attack() { + return `${this.name} attacks for ${10 + this.level * 2} damage!` + } + } + } + + const alice = createPlayer('Alice') + const bob = createPlayer('Bob') + + expect(alice.name).toBe('Alice') + expect(bob.name).toBe('Bob') + expect(alice.health).toBe(100) + expect(alice.attack()).toBe('Alice attacks for 12 damage!') + }) + + it('should create independent objects', () => { + function createCounter() { + return { + count: 0, + increment() { + this.count++ + } + } + } + + const counter1 = createCounter() + const counter2 = createCounter() + + counter1.increment() + counter1.increment() + counter1.increment() + counter2.increment() + + expect(counter1.count).toBe(3) + expect(counter2.count).toBe(1) + }) + }) + + describe('Factory with Multiple Parameters', () => { + it('should accept multiple parameters', () => { + function createEnemy(name, health, attackPower) { + return { + name, + health, + attackPower, + isAlive: true, + attack(target) { + return `${this.name} attacks ${target.name} for ${this.attackPower} damage!` + }, + takeDamage(amount) { + this.health -= amount + if (this.health <= 0) { + this.health = 0 + this.isAlive = false + } + return this.health + } + } + } + + const goblin = createEnemy('Goblin', 50, 10) + const dragon = createEnemy('Dragon', 500, 50) + + expect(goblin.health).toBe(50) + expect(dragon.health).toBe(500) + expect(goblin.attack(dragon)).toBe('Goblin attacks Dragon for 10 damage!') + }) + }) + + describe('Factory with Configuration Object', () => { + it('should use defaults and merge with config', () => { + function createCharacter(config = {}) { + const defaults = { + name: 'Unknown', + health: 100, + attackPower: 10, + defense: 5 + } + + return { ...defaults, ...config } + } + + const warrior = createCharacter({ name: 'Warrior', health: 150, defense: 20 }) + const mage = createCharacter({ name: 'Mage', attackPower: 30 }) + const villager = createCharacter() + + expect(warrior.name).toBe('Warrior') + expect(warrior.health).toBe(150) + expect(warrior.defense).toBe(20) + expect(warrior.attackPower).toBe(10) // default + + expect(mage.attackPower).toBe(30) + expect(mage.health).toBe(100) // default + + expect(villager.name).toBe('Unknown') + }) + }) + + describe('Factory with Private Variables (Closures)', () => { + it('should create truly private variables', () => { + function createBankAccount(initialBalance = 0) { + let balance = initialBalance + + return { + deposit(amount) { + if (amount > 0) balance += amount + return balance + }, + withdraw(amount) { + if (amount > 0 && amount <= balance) { + balance -= amount + } + return balance + }, + getBalance() { + return balance + } + } + } + + const account = createBankAccount(1000) + + expect(account.getBalance()).toBe(1000) + expect(account.deposit(500)).toBe(1500) + expect(account.withdraw(200)).toBe(1300) + + // Private variable is not accessible + expect(account.balance).toBe(undefined) + + // Can't modify balance directly + account.balance = 1000000 + expect(account.getBalance()).toBe(1300) + }) + + it('should keep transaction history private', () => { + function createAccount() { + let balance = 0 + const history = [] + + return { + deposit(amount) { + balance += amount + history.push({ type: 'deposit', amount }) + return balance + }, + getHistory() { + return [...history] // return copy + } + } + } + + const account = createAccount() + account.deposit(100) + account.deposit(50) + + const history = account.getHistory() + expect(history).toHaveLength(2) + expect(history[0]).toEqual({ type: 'deposit', amount: 100 }) + + // Modifying returned array doesn't affect internal state + history.push({ type: 'fake', amount: 9999 }) + expect(account.getHistory()).toHaveLength(2) + + // Can't access history directly + expect(account.history).toBe(undefined) + }) + + it('should have private functions', () => { + function createCounter() { + let count = 0 + + function logChange(action) { + return `[LOG] ${action}: count is now ${count}` + } + + return { + increment() { + count++ + return logChange('increment') + }, + getCount() { + return count + } + } + } + + const counter = createCounter() + + expect(counter.increment()).toBe('[LOG] increment: count is now 1') + expect(counter.getCount()).toBe(1) + + // Private function is not accessible + expect(counter.logChange).toBe(undefined) + }) + }) + + describe('Factory Creating Different Types', () => { + it('should return different object types based on input', () => { + function createWeapon(type) { + const weapons = { + sword: { name: 'Sword', damage: 25, type: 'melee' }, + bow: { name: 'Bow', damage: 20, type: 'ranged', range: 100 }, + staff: { name: 'Staff', damage: 35, type: 'magic', manaCost: 10 } + } + + if (!weapons[type]) { + throw new Error(`Unknown weapon: ${type}`) + } + + return { ...weapons[type] } + } + + const sword = createWeapon('sword') + const bow = createWeapon('bow') + const staff = createWeapon('staff') + + expect(sword.damage).toBe(25) + expect(bow.range).toBe(100) + expect(staff.manaCost).toBe(10) + + expect(() => createWeapon('laser')).toThrow('Unknown weapon: laser') + }) + }) + }) + + // =========================================== + // Part 3: Constructor Functions + // =========================================== + + describe('Part 3: Constructor Functions', () => { + describe('Basic Constructor Function', () => { + it('should create objects with new keyword', () => { + function Player(name) { + this.name = name + this.health = 100 + this.level = 1 + } + + const alice = new Player('Alice') + const bob = new Player('Bob') + + expect(alice.name).toBe('Alice') + expect(bob.name).toBe('Bob') + expect(alice.health).toBe(100) + }) + + it('should work with instanceof', () => { + function Player(name) { + this.name = name + } + + function Enemy(name) { + this.name = name + } + + const alice = new Player('Alice') + const goblin = new Enemy('Goblin') + + expect(alice instanceof Player).toBe(true) + expect(alice instanceof Enemy).toBe(false) + expect(goblin instanceof Enemy).toBe(true) + expect(goblin instanceof Object).toBe(true) + }) + }) + + describe('The new Keyword', () => { + it('should simulate what new does', () => { + function myNew(Constructor, ...args) { + const obj = Object.create(Constructor.prototype) + const result = Constructor.apply(obj, args) + return typeof result === 'object' && result !== null ? result : obj + } + + function Player(name) { + this.name = name + this.health = 100 + } + + Player.prototype.attack = function () { + return `${this.name} attacks!` + } + + const player = myNew(Player, 'Alice') + + expect(player.name).toBe('Alice') + expect(player.health).toBe(100) + expect(player.attack()).toBe('Alice attacks!') + expect(player instanceof Player).toBe(true) + }) + + it('should return custom object if constructor returns one', () => { + function ReturnsObject() { + this.value = 42 + return { custom: 'object' } + } + + function ReturnsPrimitive() { + this.value = 42 + return 'ignored' + } + + const obj1 = new ReturnsObject() + const obj2 = new ReturnsPrimitive() + + expect(obj1).toEqual({ custom: 'object' }) + expect(obj2.value).toBe(42) // primitive return is ignored + }) + }) + + describe('Prototype Methods', () => { + it('should share methods via prototype', () => { + function Player(name) { + this.name = name + } + + Player.prototype.attack = function () { + return `${this.name} attacks!` + } + + const p1 = new Player('Alice') + const p2 = new Player('Bob') + + // Methods are shared + expect(p1.attack).toBe(p2.attack) + expect(p1.attack()).toBe('Alice attacks!') + expect(p2.attack()).toBe('Bob attacks!') + }) + + it('should add multiple methods to prototype', () => { + function Character(name, health) { + this.name = name + this.health = health + } + + Character.prototype.attack = function () { + return `${this.name} attacks!` + } + + Character.prototype.takeDamage = function (amount) { + this.health -= amount + return this.health + } + + Character.prototype.isAlive = function () { + return this.health > 0 + } + + const hero = new Character('Hero', 100) + + expect(hero.attack()).toBe('Hero attacks!') + expect(hero.takeDamage(30)).toBe(70) + expect(hero.isAlive()).toBe(true) + expect(hero.takeDamage(80)).toBe(-10) + expect(hero.isAlive()).toBe(false) + }) + }) + }) + + // =========================================== + // Part 4: ES6 Classes + // =========================================== + + describe('Part 4: ES6 Classes', () => { + describe('Basic Class Syntax', () => { + it('should create objects with class syntax', () => { + class Player { + constructor(name) { + this.name = name + this.health = 100 + this.level = 1 + } + + attack() { + return `${this.name} attacks for ${10 + this.level * 2} damage!` + } + } + + const alice = new Player('Alice') + + expect(alice.name).toBe('Alice') + expect(alice.health).toBe(100) + expect(alice.attack()).toBe('Alice attacks for 12 damage!') + expect(alice instanceof Player).toBe(true) + }) + + it('should share methods via prototype (like constructors)', () => { + class Player { + constructor(name) { + this.name = name + } + + attack() { + return `${this.name} attacks!` + } + } + + const p1 = new Player('Alice') + const p2 = new Player('Bob') + + expect(p1.attack).toBe(p2.attack) // Shared via prototype + }) + }) + + describe('Class Fields', () => { + it('should support class fields with default values', () => { + class Character { + level = 1 + experience = 0 + + constructor(name) { + this.name = name + } + } + + const hero = new Character('Hero') + + expect(hero.level).toBe(1) + expect(hero.experience).toBe(0) + expect(hero.name).toBe('Hero') + }) + }) + + describe('Static Methods and Properties', () => { + it('should define static methods on class', () => { + class MathUtils { + static PI = 3.14159 + + static square(x) { + return x * x + } + + static cube(x) { + return x * x * x + } + } + + expect(MathUtils.PI).toBe(3.14159) + expect(MathUtils.square(5)).toBe(25) + expect(MathUtils.cube(3)).toBe(27) + + // Not available on instances + const utils = new MathUtils() + expect(utils.PI).toBe(undefined) + expect(utils.square).toBe(undefined) + }) + + it('should use static factory methods', () => { + class User { + constructor(id, name) { + this.id = id + this.name = name + } + + static createGuest() { + return new User(0, 'Guest') + } + + static fromData(data) { + return new User(data.id, data.name) + } + } + + const guest = User.createGuest() + const user = User.fromData({ id: 1, name: 'Alice' }) + + expect(guest.id).toBe(0) + expect(guest.name).toBe('Guest') + expect(user.id).toBe(1) + expect(user.name).toBe('Alice') + }) + }) + + describe('Getters and Setters', () => { + it('should define getters and setters', () => { + class Circle { + constructor(radius) { + this._radius = radius + } + + get radius() { + return this._radius + } + + set radius(value) { + if (value < 0) throw new Error('Radius cannot be negative') + this._radius = value + } + + get diameter() { + return this._radius * 2 + } + + set diameter(value) { + this._radius = value / 2 + } + + get area() { + return Math.PI * this._radius ** 2 + } + } + + const circle = new Circle(5) + + expect(circle.radius).toBe(5) + expect(circle.diameter).toBe(10) + expect(circle.area).toBeCloseTo(78.54, 1) + + circle.diameter = 20 + expect(circle.radius).toBe(10) + + expect(() => { + circle.radius = -5 + }).toThrow('Radius cannot be negative') + }) + }) + + describe('Private Fields (#)', () => { + it('should create truly private fields', () => { + class BankAccount { + #balance = 0 + + constructor(initialBalance) { + this.#balance = initialBalance + } + + deposit(amount) { + if (amount > 0) this.#balance += amount + return this.#balance + } + + getBalance() { + return this.#balance + } + } + + const account = new BankAccount(1000) + + expect(account.getBalance()).toBe(1000) + expect(account.deposit(500)).toBe(1500) + + // Private field is not accessible + expect(account.balance).toBe(undefined) + expect(account['#balance']).toBe(undefined) + }) + + it('should support private methods', () => { + class Counter { + #count = 0 + + #log(action) { + return `[${action}] count: ${this.#count}` + } + + increment() { + this.#count++ + return this.#log('increment') + } + + getCount() { + return this.#count + } + } + + const counter = new Counter() + + expect(counter.increment()).toBe('[increment] count: 1') + expect(counter.getCount()).toBe(1) + + // Private method is not accessible + expect(counter.log).toBe(undefined) + }) + }) + + describe('Classes are Syntactic Sugar', () => { + it('should prove classes are functions', () => { + class Player { + constructor(name) { + this.name = name + } + } + + expect(typeof Player).toBe('function') + }) + + it('should have same prototype behavior as constructor functions', () => { + class Player { + constructor(name) { + this.name = name + } + + attack() { + return `${this.name} attacks!` + } + } + + const player = new Player('Alice') + + expect(player.constructor).toBe(Player) + expect(Object.getPrototypeOf(player)).toBe(Player.prototype) + expect(player.__proto__).toBe(Player.prototype) + }) + }) + }) + + // =========================================== + // Part 5: Inheritance + // =========================================== + + describe('Part 5: Inheritance', () => { + describe('Class Inheritance with extends', () => { + it('should inherit from parent class', () => { + class Character { + constructor(name, health) { + this.name = name + this.health = health + } + + attack() { + return `${this.name} attacks!` + } + + takeDamage(amount) { + this.health -= amount + return this.health + } + } + + class Warrior extends Character { + constructor(name) { + super(name, 150) // Call parent constructor + this.armor = 20 + } + + shieldBash() { + return `${this.name} bashes with shield for ${this.armor} damage!` + } + } + + const conan = new Warrior('Conan') + + expect(conan.name).toBe('Conan') + expect(conan.health).toBe(150) + expect(conan.armor).toBe(20) + expect(conan.attack()).toBe('Conan attacks!') + expect(conan.shieldBash()).toBe('Conan bashes with shield for 20 damage!') + }) + + it('should work with instanceof through inheritance chain', () => { + class Animal {} + class Dog extends Animal {} + + const rex = new Dog() + + expect(rex instanceof Dog).toBe(true) + expect(rex instanceof Animal).toBe(true) + expect(rex instanceof Object).toBe(true) + }) + }) + + describe('Method Overriding', () => { + it('should override parent methods', () => { + class Animal { + speak() { + return 'Some sound' + } + } + + class Dog extends Animal { + speak() { + return 'Woof!' + } + } + + class Cat extends Animal { + speak() { + return 'Meow!' + } + } + + const animal = new Animal() + const dog = new Dog() + const cat = new Cat() + + expect(animal.speak()).toBe('Some sound') + expect(dog.speak()).toBe('Woof!') + expect(cat.speak()).toBe('Meow!') + }) + + it('should call parent method with super', () => { + class Character { + constructor(name) { + this.name = name + } + + attack() { + return `${this.name} attacks!` + } + } + + class Warrior extends Character { + attack() { + return `${super.attack()} With great strength!` + } + } + + const warrior = new Warrior('Conan') + + expect(warrior.attack()).toBe('Conan attacks! With great strength!') + }) + }) + + describe('The super Keyword', () => { + it('should require super() before using this in derived class', () => { + class Parent { + constructor(name) { + this.name = name + } + } + + class Child extends Parent { + constructor(name, age) { + super(name) // Must call before using this + this.age = age + } + } + + const child = new Child('Alice', 10) + + expect(child.name).toBe('Alice') + expect(child.age).toBe(10) + }) + }) + + describe('Factory Composition', () => { + it('should compose behaviors from multiple sources', () => { + const canWalk = (state) => ({ + walk() { + state.position += state.speed + return `${state.name} walks to position ${state.position}` + } + }) + + const canSwim = (state) => ({ + swim() { + state.position += state.speed * 1.5 + return `${state.name} swims to position ${state.position}` + } + }) + + const canFly = (state) => ({ + fly() { + state.position += state.speed * 3 + return `${state.name} flies to position ${state.position}` + } + }) + + function createDuck(name) { + const state = { name, position: 0, speed: 2 } + return { + name: state.name, + ...canWalk(state), + ...canSwim(state), + ...canFly(state), + getPosition: () => state.position + } + } + + function createPenguin(name) { + const state = { name, position: 0, speed: 1 } + return { + name: state.name, + ...canWalk(state), + ...canSwim(state), + // No fly! + getPosition: () => state.position + } + } + + const duck = createDuck('Donald') + const penguin = createPenguin('Tux') + + expect(duck.walk()).toBe('Donald walks to position 2') + expect(duck.swim()).toBe('Donald swims to position 5') + expect(duck.fly()).toBe('Donald flies to position 11') + + expect(penguin.walk()).toBe('Tux walks to position 1') + expect(penguin.swim()).toBe('Tux swims to position 2.5') + expect(penguin.fly).toBe(undefined) // Penguins can't fly + }) + + it('should support canSpeak behavior composition', () => { + const canSpeak = (state) => ({ + speak(message) { + return `${state.name} says: "${message}"` + } + }) + + const canWalk = (state) => ({ + walk() { + state.position += state.speed + return `${state.name} walks to position ${state.position}` + } + }) + + function createDuck(name) { + const state = { name, position: 0, speed: 2 } + return { + name: state.name, + ...canWalk(state), + ...canSpeak(state), + getPosition: () => state.position + } + } + + function createFish(name) { + const state = { name, position: 0, speed: 4 } + return { + name: state.name, + // Fish can't speak! + getPosition: () => state.position + } + } + + const duck = createDuck('Donald') + const fish = createFish('Nemo') + + expect(duck.speak('Quack!')).toBe('Donald says: "Quack!"') + expect(duck.walk()).toBe('Donald walks to position 2') + + expect(fish.speak).toBe(undefined) // Fish can't speak + }) + + it('should allow flexible behavior combinations', () => { + const withHealth = (state) => ({ + takeDamage(amount) { + state.health -= amount + return state.health + }, + heal(amount) { + state.health = Math.min(state.maxHealth, state.health + amount) + return state.health + }, + getHealth: () => state.health, + isAlive: () => state.health > 0 + }) + + const withMana = (state) => ({ + useMana(amount) { + if (state.mana >= amount) { + state.mana -= amount + return true + } + return false + }, + getMana: () => state.mana + }) + + function createWarrior(name) { + const state = { name, health: 150, maxHealth: 150 } + return { + name: state.name, + ...withHealth(state) + // No mana for warriors + } + } + + function createMage(name) { + const state = { name, health: 80, maxHealth: 80, mana: 100 } + return { + name: state.name, + ...withHealth(state), + ...withMana(state) + } + } + + const warrior = createWarrior('Conan') + const mage = createMage('Gandalf') + + expect(warrior.getHealth()).toBe(150) + expect(warrior.takeDamage(50)).toBe(100) + expect(warrior.getMana).toBe(undefined) // Warriors don't have mana + + expect(mage.getHealth()).toBe(80) + expect(mage.getMana()).toBe(100) + expect(mage.useMana(30)).toBe(true) + expect(mage.getMana()).toBe(70) + }) + }) + }) + + // =========================================== + // Part 6: Factory vs Class Comparison + // =========================================== + + describe('Part 6: Factory vs Class Comparison', () => { + describe('instanceof behavior', () => { + it('should work with classes but not factories', () => { + class ClassPlayer { + constructor(name) { + this.name = name + } + } + + function createPlayer(name) { + return { name } + } + + const classPlayer = new ClassPlayer('Alice') + const factoryPlayer = createPlayer('Bob') + + expect(classPlayer instanceof ClassPlayer).toBe(true) + expect(factoryPlayer instanceof Object).toBe(true) + // Factory objects are just plain objects + }) + }) + + describe('Memory efficiency', () => { + it('should show classes share prototype methods', () => { + class ClassPlayer { + attack() { + return 'attack' + } + } + + const p1 = new ClassPlayer() + const p2 = new ClassPlayer() + + expect(p1.attack).toBe(p2.attack) // Same function reference + }) + + it('should show factories create new methods for each instance', () => { + function createPlayer() { + return { + attack() { + return 'attack' + } + } + } + + const p1 = createPlayer() + const p2 = createPlayer() + + expect(p1.attack).not.toBe(p2.attack) // Different function references + }) + }) + + describe('Privacy comparison', () => { + it('should show both can achieve true privacy', () => { + // Class with private fields + class ClassWallet { + #balance = 0 + + deposit(amount) { + this.#balance += amount + } + getBalance() { + return this.#balance + } + } + + // Factory with closures + function createWallet() { + let balance = 0 + return { + deposit(amount) { + balance += amount + }, + getBalance() { + return balance + } + } + } + + const classWallet = new ClassWallet() + const factoryWallet = createWallet() + + classWallet.deposit(100) + factoryWallet.deposit(100) + + expect(classWallet.getBalance()).toBe(100) + expect(factoryWallet.getBalance()).toBe(100) + + // Both are truly private + expect(classWallet.balance).toBe(undefined) + expect(factoryWallet.balance).toBe(undefined) + }) + }) + }) + + // =========================================== + // Common Mistakes + // =========================================== + + describe('Common Mistakes', () => { + describe('Mistake 1: Forgetting new with Constructor Functions', () => { + it('should throw or behave unexpectedly when new is forgotten (strict mode)', () => { + function Player(name) { + this.name = name + this.health = 100 + } + + // In strict mode (which modern JS uses), forgetting 'new' throws an error + // because 'this' is undefined, not the global object + expect(() => Player('Alice')).toThrow() + }) + + it('should work correctly with new keyword', () => { + function Player(name) { + this.name = name + this.health = 100 + } + + const bob = new Player('Bob') + + expect(bob.name).toBe('Bob') + expect(bob.health).toBe(100) + expect(bob instanceof Player).toBe(true) + }) + + it('should throw error when calling class without new', () => { + class Player { + constructor(name) { + this.name = name + } + } + + // Classes protect against this mistake + expect(() => Player('Alice')).toThrow() + }) + }) + + describe('Mistake 3: Underscore Convention vs True Privacy', () => { + it('should show underscore properties ARE accessible (not truly private)', () => { + class BankAccount { + constructor(balance) { + this._balance = balance // Convention only, NOT private! + } + + getBalance() { + return this._balance + } + } + + const account = new BankAccount(1000) + + // Underscore properties are fully accessible! + expect(account._balance).toBe(1000) + + // Can be modified directly + account._balance = 999999 + expect(account.getBalance()).toBe(999999) + }) + + it('should show private fields (#) are truly private', () => { + class SecureBankAccount { + #balance // Truly private + + constructor(balance) { + this.#balance = balance + } + + getBalance() { + return this.#balance + } + } + + const secure = new SecureBankAccount(1000) + + // Private field is not accessible + expect(secure.balance).toBe(undefined) + + // Can only access via methods + expect(secure.getBalance()).toBe(1000) + }) + }) + + describe('Mistake 4: Using this Incorrectly in Factory Functions', () => { + it('should show this can break when method is extracted', () => { + function createCounter() { + return { + count: 0, + increment() { + this.count++ // 'this' depends on how method is called + } + } + } + + const counter = createCounter() + counter.increment() // Works - this is counter + expect(counter.count).toBe(1) + + // Extract the method + const increment = counter.increment + + // Call without context - 'this' is undefined in strict mode + // This won't modify counter.count + try { + increment() + } catch (e) { + // In strict mode, this throws because this is undefined + } + + // counter.count is still 1 because the extracted call didn't work + expect(counter.count).toBe(1) + }) + + it('should show closures avoid this problem', () => { + function createSafeCounter() { + let count = 0 // Closure variable - no 'this' needed + + return { + increment() { + count++ // Uses closure, not this + }, + getCount() { + return count + } + } + } + + const counter = createSafeCounter() + counter.increment() + expect(counter.getCount()).toBe(1) + + // Extract the method + const increment = counter.increment + + // Works even when extracted! + increment() + expect(counter.getCount()).toBe(2) + }) + }) + }) + + // =========================================== + // Additional Edge Cases + // =========================================== + + describe('Edge Cases', () => { + describe('Class Expression', () => { + it('should support class expressions', () => { + const Player = class { + constructor(name) { + this.name = name + } + } + + const alice = new Player('Alice') + expect(alice.name).toBe('Alice') + }) + + it('should support named class expressions', () => { + const Player = class PlayerClass { + constructor(name) { + this.name = name + } + + static getClassName() { + return PlayerClass.name + } + } + + expect(Player.name).toBe('PlayerClass') + expect(Player.getClassName()).toBe('PlayerClass') + }) + }) + + describe('Extending Built-in Objects', () => { + it('should extend Array', () => { + class ExtendedArray extends Array { + get first() { + return this[0] + } + + get last() { + return this[this.length - 1] + } + + sum() { + return this.reduce((a, b) => a + b, 0) + } + } + + const arr = new ExtendedArray(1, 2, 3, 4, 5) + + expect(arr.first).toBe(1) + expect(arr.last).toBe(5) + expect(arr.sum()).toBe(15) + expect(arr instanceof Array).toBe(true) + expect(arr instanceof ExtendedArray).toBe(true) + + // Array methods still work and return ExtendedArray + const doubled = arr.map((x) => x * 2) + expect(doubled instanceof ExtendedArray).toBe(true) + expect(doubled.sum()).toBe(30) + }) + }) + + describe('Static Initialization Blocks', () => { + it('should support static initialization blocks', () => { + class Config { + static values = {} + + static { + Config.values.initialized = true + Config.values.timestamp = Date.now() + } + } + + expect(Config.values.initialized).toBe(true) + expect(typeof Config.values.timestamp).toBe('number') + }) + }) + + describe('Factory with Validation', () => { + it('should validate input in factory', () => { + function createUser(name, age) { + if (typeof name !== 'string' || name.length === 0) { + throw new Error('Name must be a non-empty string') + } + if (typeof age !== 'number' || age < 0) { + throw new Error('Age must be a positive number') + } + + return { + name, + age, + isAdult: age >= 18 + } + } + + const user = createUser('Alice', 25) + expect(user.name).toBe('Alice') + expect(user.isAdult).toBe(true) + + expect(() => createUser('', 25)).toThrow('Name must be a non-empty string') + expect(() => createUser('Alice', -5)).toThrow('Age must be a positive number') + }) + }) + + describe('Arrow Function Class Fields', () => { + it('should auto-bind this with arrow function class fields', () => { + class Button { + count = 0 + + // Arrow function automatically binds 'this' to the instance + handleClick = () => { + this.count++ + return this.count + } + } + + const button = new Button() + + // Works when called directly + expect(button.handleClick()).toBe(1) + + // Extract the method + const handler = button.handleClick + + // Works even when extracted! 'this' is still bound to button + expect(handler()).toBe(2) + expect(button.count).toBe(2) + }) + + it('should show regular methods lose this when extracted', () => { + class Counter { + count = 0 + + // Regular method - 'this' depends on call context + increment() { + this.count++ + return this.count + } + } + + const counter = new Counter() + expect(counter.increment()).toBe(1) + + // Extract the method + const increment = counter.increment + + // 'this' is undefined in strict mode when called standalone + expect(() => increment()).toThrow() + }) + }) + + describe('Method Chaining', () => { + it('should support method chaining in class', () => { + class Builder { + constructor() { + this.result = {} + } + + setName(name) { + this.result.name = name + return this + } + + setAge(age) { + this.result.age = age + return this + } + + build() { + return this.result + } + } + + const person = new Builder().setName('Alice').setAge(25).build() + + expect(person).toEqual({ name: 'Alice', age: 25 }) + }) + + it('should support method chaining in factory', () => { + function createBuilder() { + const result = {} + + return { + setName(name) { + result.name = name + return this + }, + setAge(age) { + result.age = age + return this + }, + build() { + return { ...result } + } + } + } + + const person = createBuilder().setName('Bob').setAge(30).build() + + expect(person).toEqual({ name: 'Bob', age: 30 }) + }) + }) + }) +}) diff --git a/tests/object-oriented/inheritance-polymorphism/inheritance-polymorphism.test.js b/tests/object-oriented/inheritance-polymorphism/inheritance-polymorphism.test.js new file mode 100644 index 00000000..8e0431c7 --- /dev/null +++ b/tests/object-oriented/inheritance-polymorphism/inheritance-polymorphism.test.js @@ -0,0 +1,657 @@ +import { describe, it, expect } from 'vitest' + +describe('Inheritance & Polymorphism', () => { + // ============================================================ + // BASE CLASSES FOR TESTING + // ============================================================ + + class Character { + constructor(name, health = 100) { + this.name = name + this.health = health + } + + introduce() { + return `I am ${this.name} with ${this.health} HP` + } + + attack() { + return `${this.name} attacks!` + } + + takeDamage(amount) { + this.health -= amount + return `${this.name} takes ${amount} damage! (${this.health} HP left)` + } + + get isAlive() { + return this.health > 0 + } + + static createRandom() { + const names = ['Hero', 'Villain', 'Sidekick'] + return new this(names[Math.floor(Math.random() * names.length)]) + } + } + + class Warrior extends Character { + constructor(name) { + super(name, 150) // Warriors have more health + this.rage = 0 + this.weapon = 'Sword' + } + + attack() { + return `${this.name} swings a mighty sword!` + } + + battleCry() { + this.rage += 10 + return `${this.name} roars with fury! Rage: ${this.rage}` + } + } + + class Mage extends Character { + constructor(name) { + super(name, 80) // Mages have less health + this.mana = 100 + } + + attack() { + return `${this.name} casts a fireball!` + } + + castSpell(spell) { + this.mana -= 10 + return `${this.name} casts ${spell}!` + } + } + + class Archer extends Character { + constructor(name) { + super(name, 90) + this.arrows = 20 + } + + attack() { + this.arrows-- + return `${this.name} fires an arrow!` + } + } + + // ============================================================ + // CLASS INHERITANCE WITH EXTENDS + // ============================================================ + + describe('Class Inheritance with extends', () => { + it('should inherit properties from parent class', () => { + const warrior = new Warrior('Conan') + + // Inherited from Character + expect(warrior.name).toBe('Conan') + expect(warrior.health).toBe(150) // Custom value passed to super() + + // Unique to Warrior + expect(warrior.rage).toBe(0) + expect(warrior.weapon).toBe('Sword') + }) + + it('should inherit methods from parent class', () => { + const warrior = new Warrior('Conan') + + // Inherited method works + expect(warrior.introduce()).toBe('I am Conan with 150 HP') + expect(warrior.takeDamage(20)).toBe('Conan takes 20 damage! (130 HP left)') + }) + + it('should inherit getters from parent class', () => { + const warrior = new Warrior('Conan') + + expect(warrior.isAlive).toBe(true) + warrior.health = 0 + expect(warrior.isAlive).toBe(false) + }) + + it('should inherit static methods from parent class', () => { + const randomWarrior = Warrior.createRandom() + + expect(randomWarrior).toBeInstanceOf(Warrior) + expect(randomWarrior).toBeInstanceOf(Character) + expect(['Hero', 'Villain', 'Sidekick']).toContain(randomWarrior.name) + }) + + it('should allow child classes to have unique methods', () => { + const warrior = new Warrior('Conan') + const mage = new Mage('Gandalf') + + // Warrior-specific method + expect(warrior.battleCry()).toBe('Conan roars with fury! Rage: 10') + expect(typeof mage.battleCry).toBe('undefined') + + // Mage-specific method + expect(mage.castSpell('Fireball')).toBe('Gandalf casts Fireball!') + expect(typeof warrior.castSpell).toBe('undefined') + }) + }) + + // ============================================================ + // THE SUPER KEYWORD + // ============================================================ + + describe('The super Keyword', () => { + it('super() should call parent constructor with arguments', () => { + const warrior = new Warrior('Conan') + + // super(name, 150) was called in Warrior constructor + expect(warrior.name).toBe('Conan') + expect(warrior.health).toBe(150) + }) + + it('super.method() should call parent method', () => { + class ExtendedWarrior extends Character { + constructor(name) { + super(name, 150) + this.weapon = 'Axe' + } + + attack() { + const baseAttack = super.attack() // "Name attacks!" + return `${baseAttack} With an ${this.weapon}!` + } + + describe() { + return `${super.introduce()} - Warrior Class` + } + } + + const hero = new ExtendedWarrior('Gimli') + + expect(hero.attack()).toBe('Gimli attacks! With an Axe!') + expect(hero.describe()).toBe('I am Gimli with 150 HP - Warrior Class') + }) + + it('should throw ReferenceError if super() is not called before this', () => { + // This would cause an error - we test the concept + expect(() => { + class BrokenWarrior extends Character { + constructor(name) { + // Intentionally not calling super() first + // this.rage = 0 // Would throw ReferenceError + super(name) + } + } + new BrokenWarrior('Test') + }).not.toThrow() // The fixed version doesn't throw + }) + }) + + // ============================================================ + // METHOD OVERRIDING + // ============================================================ + + describe('Method Overriding', () => { + it('should override parent method with child implementation', () => { + const character = new Character('Generic') + const warrior = new Warrior('Conan') + const mage = new Mage('Gandalf') + const archer = new Archer('Legolas') + + // Each class has different attack() implementation + expect(character.attack()).toBe('Generic attacks!') + expect(warrior.attack()).toBe('Conan swings a mighty sword!') + expect(mage.attack()).toBe('Gandalf casts a fireball!') + expect(archer.attack()).toBe('Legolas fires an arrow!') + }) + + it('should allow extending parent behavior with super.method()', () => { + class VerboseWarrior extends Character { + attack() { + return `${super.attack()} POWERFULLY!` + } + } + + const hero = new VerboseWarrior('Hero') + expect(hero.attack()).toBe('Hero attacks! POWERFULLY!') + }) + + it('should allow complete replacement of parent behavior', () => { + class SilentWarrior extends Character { + attack() { + return '...' // Completely different, no super.attack() + } + } + + const ninja = new SilentWarrior('Shadow') + expect(ninja.attack()).toBe('...') + }) + }) + + // ============================================================ + // POLYMORPHISM + // ============================================================ + + describe('Polymorphism', () => { + it('should treat different types uniformly through common interface', () => { + const party = [ + new Warrior('Conan'), + new Mage('Gandalf'), + new Archer('Legolas'), + new Character('Villager') + ] + + // All can attack(), each in their own way + const attacks = party.map(char => char.attack()) + + expect(attacks).toEqual([ + 'Conan swings a mighty sword!', + 'Gandalf casts a fireball!', + 'Legolas fires an arrow!', + 'Villager attacks!' + ]) + }) + + it('should allow functions to work with any subtype', () => { + function executeBattle(characters) { + return characters.map(char => char.attack()) + } + + const team1 = [new Warrior('W1'), new Warrior('W2')] + const team2 = [new Mage('M1'), new Archer('A1')] + const mixedTeam = [new Warrior('W'), new Mage('M'), new Archer('A')] + + // Same function works with any combination + expect(executeBattle(team1)).toHaveLength(2) + expect(executeBattle(team2)).toHaveLength(2) + expect(executeBattle(mixedTeam)).toHaveLength(3) + }) + + it('instanceof should check entire prototype chain', () => { + const warrior = new Warrior('Conan') + + expect(warrior instanceof Warrior).toBe(true) + expect(warrior instanceof Character).toBe(true) + expect(warrior instanceof Object).toBe(true) + expect(warrior instanceof Mage).toBe(false) + }) + + it('should enable the Open/Closed principle', () => { + // We can add new character types without changing existing code + class Healer extends Character { + attack() { + return `${this.name} heals the party!` + } + } + + // Existing function works with new type + function getAttacks(chars) { + return chars.map(c => c.attack()) + } + + const team = [new Warrior('W'), new Healer('H')] + const attacks = getAttacks(team) + + expect(attacks).toContain('W swings a mighty sword!') + expect(attacks).toContain('H heals the party!') + }) + }) + + // ============================================================ + // PROTOTYPE CHAIN (Under the Hood) + // ============================================================ + + describe('Prototype Chain', () => { + it('should set up prototype chain correctly with extends', () => { + const warrior = new Warrior('Conan') + + // Instance -> Warrior.prototype -> Character.prototype -> Object.prototype + expect(Object.getPrototypeOf(warrior)).toBe(Warrior.prototype) + expect(Object.getPrototypeOf(Warrior.prototype)).toBe(Character.prototype) + expect(Object.getPrototypeOf(Character.prototype)).toBe(Object.prototype) + }) + + it('should find methods by walking up the prototype chain', () => { + const warrior = new Warrior('Conan') + + // attack() is on Warrior.prototype (overridden) + expect(Warrior.prototype.hasOwnProperty('attack')).toBe(true) + + // introduce() is on Character.prototype (inherited) + expect(Warrior.prototype.hasOwnProperty('introduce')).toBe(false) + expect(Character.prototype.hasOwnProperty('introduce')).toBe(true) + + // Both work on the instance + expect(warrior.attack()).toContain('sword') + expect(warrior.introduce()).toContain('Conan') + }) + }) + + // ============================================================ + // COMPOSITION PATTERN + // ============================================================ + + describe('Composition Pattern', () => { + it('should compose behaviors instead of inheriting', () => { + // Behavior factories + const canFly = (state) => ({ + fly() { return `${state.name} soars through the sky!` } + }) + + const canCast = (state) => ({ + castSpell(spell) { return `${state.name} casts ${spell}!` } + }) + + const canFight = (state) => ({ + attack() { return `${state.name} attacks!` } + }) + + // Compose a flying mage + function createFlyingMage(name) { + const state = { name, health: 100, mana: 50 } + return { + ...state, + ...canFly(state), + ...canCast(state), + ...canFight(state) + } + } + + const merlin = createFlyingMage('Merlin') + + expect(merlin.fly()).toBe('Merlin soars through the sky!') + expect(merlin.castSpell('Ice')).toBe('Merlin casts Ice!') + expect(merlin.attack()).toBe('Merlin attacks!') + expect(merlin.health).toBe(100) + expect(merlin.mana).toBe(50) + }) + + it('should allow mixing and matching behaviors freely', () => { + const canSwim = (state) => ({ + swim() { return `${state.name} swims!` } + }) + + const canFly = (state) => ({ + fly() { return `${state.name} flies!` } + }) + + // Duck can both swim and fly + function createDuck(name) { + const state = { name } + return { ...state, ...canSwim(state), ...canFly(state) } + } + + // Fish can only swim + function createFish(name) { + const state = { name } + return { ...state, ...canSwim(state) } + } + + const duck = createDuck('Donald') + const fish = createFish('Nemo') + + expect(duck.swim()).toBe('Donald swims!') + expect(duck.fly()).toBe('Donald flies!') + expect(fish.swim()).toBe('Nemo swims!') + expect(fish.fly).toBeUndefined() + }) + }) + + // ============================================================ + // MIXINS + // ============================================================ + + describe('Mixins', () => { + it('should mix behavior into class prototype with Object.assign', () => { + const Swimmer = { + swim() { return `${this.name} swims!` } + } + + const Flyer = { + fly() { return `${this.name} flies!` } + } + + class Animal { + constructor(name) { + this.name = name + } + } + + class Duck extends Animal {} + Object.assign(Duck.prototype, Swimmer, Flyer) + + const donald = new Duck('Donald') + + expect(donald.swim()).toBe('Donald swims!') + expect(donald.fly()).toBe('Donald flies!') + }) + + it('should support functional mixin pattern', () => { + const withLogging = (Base) => class extends Base { + log(message) { + return `[${this.name}]: ${message}` + } + } + + const withTimestamp = (Base) => class extends Base { + getTimestamp() { + return '2024-01-15' + } + } + + class Character { + constructor(name) { + this.name = name + } + } + + // Stack mixins + class LoggedCharacter extends withTimestamp(withLogging(Character)) { + doAction() { + return this.log(`Action at ${this.getTimestamp()}`) + } + } + + const hero = new LoggedCharacter('Aragorn') + + expect(hero.log('Hello')).toBe('[Aragorn]: Hello') + expect(hero.getTimestamp()).toBe('2024-01-15') + expect(hero.doAction()).toBe('[Aragorn]: Action at 2024-01-15') + }) + + it('should handle mixin name collisions (last one wins)', () => { + const MixinA = { + greet() { return 'Hello from A' } + } + + const MixinB = { + greet() { return 'Hello from B' } + } + + class Base {} + Object.assign(Base.prototype, MixinA, MixinB) + + const instance = new Base() + + // MixinB's greet() overwrites MixinA's + expect(instance.greet()).toBe('Hello from B') + }) + }) + + // ============================================================ + // COMMON MISTAKES + // ============================================================ + + describe('Common Mistakes', () => { + it('should demonstrate that inherited methods can be accidentally lost', () => { + class Parent { + method() { return 'parent' } + } + + class Child extends Parent { + method() { return 'child' } // Completely replaces parent + } + + const child = new Child() + expect(child.method()).toBe('child') + + // To preserve parent behavior, use super.method() + class BetterChild extends Parent { + method() { return `${super.method()} + child` } + } + + const betterChild = new BetterChild() + expect(betterChild.method()).toBe('parent + child') + }) + + it('should show the problem with inheriting for code reuse only', () => { + // BAD: Stack is NOT an Array (violates IS-A) + // A Stack should only allow push/pop, not shift/unshift + class BadStack extends Array { + peek() { return this[this.length - 1] } + } + + const badStack = new BadStack() + badStack.push(1, 2, 3) + + // Problem: Array methods we DON'T want are available + expect(badStack.shift()).toBe(1) // Stacks shouldn't allow this! + + // GOOD: Composition - Stack HAS-A array + class GoodStack { + #items = [] + + push(item) { this.#items.push(item) } + pop() { return this.#items.pop() } + peek() { return this.#items[this.#items.length - 1] } + } + + const goodStack = new GoodStack() + goodStack.push(1) + goodStack.push(2) + + expect(goodStack.peek()).toBe(2) + expect(typeof goodStack.shift).toBe('undefined') // Correctly unavailable + }) + }) + + // ============================================================ + // SHAPE POLYMORPHISM (Interview Question Example) + // ============================================================ + + describe('Shape Polymorphism (Interview Example)', () => { + class Shape { + area() { return 0 } + } + + class Rectangle extends Shape { + constructor(width, height) { + super() + this.width = width + this.height = height + } + area() { return this.width * this.height } + } + + class Circle extends Shape { + constructor(radius) { + super() + this.radius = radius + } + area() { return Math.PI * this.radius ** 2 } + } + + it('should calculate area differently for each shape type', () => { + const rectangle = new Rectangle(4, 5) + const circle = new Circle(3) + + expect(rectangle.area()).toBe(20) + expect(circle.area()).toBeCloseTo(28.274, 2) // Math.PI * 9 + }) + + it('should treat all shapes uniformly through common interface', () => { + const shapes = [new Rectangle(4, 5), new Circle(3), new Shape()] + const areas = shapes.map(s => s.area()) + + expect(areas[0]).toBe(20) + expect(areas[1]).toBeCloseTo(28.274, 2) + expect(areas[2]).toBe(0) // Base shape + }) + + it('should verify instanceof for shape hierarchy', () => { + const rect = new Rectangle(2, 3) + const circle = new Circle(5) + + expect(rect instanceof Rectangle).toBe(true) + expect(rect instanceof Shape).toBe(true) + expect(circle instanceof Circle).toBe(true) + expect(circle instanceof Shape).toBe(true) + expect(rect instanceof Circle).toBe(false) + }) + }) + + // ============================================================ + // MULTI-LEVEL INHERITANCE + // ============================================================ + + describe('Multi-level Inheritance', () => { + it('should support multi-level inheritance (keep shallow!)', () => { + class Entity { + constructor(id) { + this.id = id + } + } + + class Character extends Entity { + constructor(id, name) { + super(id) + this.name = name + } + } + + class Warrior extends Character { + constructor(id, name) { + super(id, name) + this.class = 'Warrior' + } + } + + const hero = new Warrior(1, 'Conan') + + expect(hero.id).toBe(1) + expect(hero.name).toBe('Conan') + expect(hero.class).toBe('Warrior') + + expect(hero instanceof Warrior).toBe(true) + expect(hero instanceof Character).toBe(true) + expect(hero instanceof Entity).toBe(true) + }) + + it('should call super() chain correctly', () => { + const calls = [] + + class A { + constructor() { + calls.push('A') + } + } + + class B extends A { + constructor() { + super() + calls.push('B') + } + } + + class C extends B { + constructor() { + super() + calls.push('C') + } + } + + new C() + + // Constructors called from parent to child + expect(calls).toEqual(['A', 'B', 'C']) + }) + }) +}) diff --git a/tests/object-oriented/object-creation-prototypes/object-creation-prototypes.test.js b/tests/object-oriented/object-creation-prototypes/object-creation-prototypes.test.js new file mode 100644 index 00000000..3eb3e75d --- /dev/null +++ b/tests/object-oriented/object-creation-prototypes/object-creation-prototypes.test.js @@ -0,0 +1,440 @@ +import { describe, it, expect } from 'vitest' + +describe('Object Creation & Prototypes', () => { + describe('Opening Hook - Inherited Methods', () => { + it('should have inherited methods from Object.prototype', () => { + // You create a simple object + const player = { name: 'Alice', health: 100 } + + // But it has methods you never defined! + expect(typeof player.toString).toBe('function') + expect(player.toString()).toBe('[object Object]') + expect(player.hasOwnProperty('name')).toBe(true) + + // Where do these come from? + expect(Object.getPrototypeOf(player)).toBe(Object.prototype) + }) + }) + + describe('Prototype Chain', () => { + it('should look up properties through the prototype chain', () => { + const grandparent = { familyName: 'Smith' } + const parent = Object.create(grandparent) + parent.job = 'Engineer' + const child = Object.create(parent) + child.name = 'Alice' + + // Property lookup walks the chain + expect(child.name).toBe('Alice') // found on child + expect(child.job).toBe('Engineer') // found on parent + expect(child.familyName).toBe('Smith') // found on grandparent + }) + + it('should inherit methods from prototype (wizard/apprentice example)', () => { + // Create a simple object + const wizard = { + name: 'Gandalf', + castSpell() { + return `${this.name} casts a spell!` + } + } + + // Create another object that inherits from wizard + const apprentice = Object.create(wizard) + apprentice.name = 'Harry' + + // apprentice has its own 'name' property + expect(apprentice.name).toBe('Harry') + + // But castSpell comes from the prototype (wizard) + expect(apprentice.castSpell()).toBe('Harry casts a spell!') + + // The prototype chain: + // apprentice → wizard → Object.prototype → null + expect(Object.getPrototypeOf(apprentice)).toBe(wizard) + expect(Object.getPrototypeOf(wizard)).toBe(Object.prototype) + expect(Object.getPrototypeOf(Object.prototype)).toBeNull() + }) + + it('should return undefined when property is not found in chain', () => { + const obj = { name: 'test' } + expect(obj.nonexistent).toBeUndefined() + }) + + it('should end the chain at null', () => { + const obj = {} + expect(Object.getPrototypeOf(Object.prototype)).toBeNull() + }) + + it('should shadow inherited properties when set on object', () => { + const prototype = { greeting: 'Hello', count: 0 } + const obj = Object.create(prototype) + + // Before shadowing + expect(obj.greeting).toBe('Hello') + + // Shadow the property + obj.greeting = 'Hi' + + // obj has its own property now + expect(obj.greeting).toBe('Hi') + // Prototype is unchanged + expect(prototype.greeting).toBe('Hello') + expect(obj.hasOwnProperty('greeting')).toBe(true) + }) + }) + + describe('[[Prototype]], __proto__, and .prototype', () => { + it('should have Object.prototype as prototype for plain objects', () => { + const obj = {} + expect(Object.getPrototypeOf(obj)).toBe(Object.prototype) + }) + + it('should have .prototype property only on functions', () => { + function Player(name) { + this.name = name + } + const alice = new Player('Alice') + + // Functions have .prototype + expect(Player.prototype).toBeDefined() + expect(typeof Player.prototype).toBe('object') + + // Instances don't have .prototype + expect(alice.prototype).toBeUndefined() + + // Instance's [[Prototype]] is the constructor's .prototype + expect(Object.getPrototypeOf(alice)).toBe(Player.prototype) + }) + }) + + describe('Object Literals', () => { + it('should have Object.prototype as prototype', () => { + // Object literal — prototype is automatically Object.prototype + const player = { + name: 'Alice', + health: 100, + attack() { + return `${this.name} attacks!` + } + } + + expect(Object.getPrototypeOf(player)).toBe(Object.prototype) + expect(player.attack()).toBe('Alice attacks!') + }) + }) + + describe('Object.create()', () => { + it('should create object with specified prototype', () => { + const animalProto = { + speak() { + return `${this.name} makes a sound.` + } + } + + const dog = Object.create(animalProto) + dog.name = 'Rex' + + expect(Object.getPrototypeOf(dog)).toBe(animalProto) + expect(dog.speak()).toBe('Rex makes a sound.') + }) + + it('should create object with null prototype', () => { + const dict = Object.create(null) + + // No inherited properties + expect(dict.toString).toBeUndefined() + expect(dict.hasOwnProperty).toBeUndefined() + expect(Object.getPrototypeOf(dict)).toBeNull() + + // Can use any key without collision + dict['hasOwnProperty'] = 'safe!' + expect(dict['hasOwnProperty']).toBe('safe!') + }) + + it('should create object with property descriptors', () => { + const person = Object.create(Object.prototype, { + name: { + value: 'Alice', + writable: true, + enumerable: true, + configurable: true + }, + age: { + value: 30, + writable: false, + enumerable: true, + configurable: false + } + }) + + expect(person.name).toBe('Alice') + expect(person.age).toBe(30) + + // Can modify writable property + person.name = 'Bob' + expect(person.name).toBe('Bob') + + // Cannot modify non-writable property (throws in strict mode) + expect(() => { + person.age = 25 + }).toThrow(TypeError) + expect(person.age).toBe(30) // unchanged + }) + }) + + describe('new operator', () => { + it('should create object with correct prototype', () => { + function Player(name) { + this.name = name + } + Player.prototype.greet = function () { + return `Hello, ${this.name}!` + } + + const alice = new Player('Alice') + + expect(Object.getPrototypeOf(alice)).toBe(Player.prototype) + expect(alice.greet()).toBe('Hello, Alice!') + }) + + it('should bind this to the new object', () => { + function Counter() { + this.count = 0 + this.increment = function () { + this.count++ + } + } + + const counter = new Counter() + expect(counter.count).toBe(0) + counter.increment() + expect(counter.count).toBe(1) + }) + + it('should return the object unless constructor returns an object', () => { + function ReturnsNothing(name) { + this.name = name + // Implicitly returns the new object + } + + function ReturnsPrimitive(name) { + this.name = name + return 42 // Primitive is ignored + } + + function ReturnsObject(name) { + this.name = name + return { different: true } // Object is returned instead + } + + const obj1 = new ReturnsNothing('test') + expect(obj1.name).toBe('test') + + const obj2 = new ReturnsPrimitive('test') + expect(obj2.name).toBe('test') // Primitive return ignored + + const obj3 = new ReturnsObject('test') + expect(obj3.different).toBe(true) + expect(obj3.name).toBeUndefined() // Original object not returned + }) + + it('can be simulated with Object.create and apply', () => { + function myNew(Constructor, ...args) { + const obj = Object.create(Constructor.prototype) + const result = Constructor.apply(obj, args) + return result !== null && typeof result === 'object' ? result : obj + } + + function Player(name, health) { + this.name = name + this.health = health + } + Player.prototype.attack = function () { + return `${this.name} attacks!` + } + + const player1 = new Player('Alice', 100) + const player2 = myNew(Player, 'Bob', 100) + + expect(player1.name).toBe('Alice') + expect(player2.name).toBe('Bob') + expect(player1.attack()).toBe('Alice attacks!') + expect(player2.attack()).toBe('Bob attacks!') + expect(player1 instanceof Player).toBe(true) + expect(player2 instanceof Player).toBe(true) + }) + }) + + describe('Object.assign()', () => { + it('should copy enumerable own properties', () => { + const target = { a: 1 } + const source = { b: 2, c: 3 } + + const result = Object.assign(target, source) + + expect(result).toEqual({ a: 1, b: 2, c: 3 }) + expect(result).toBe(target) // Returns the target + }) + + it('should merge multiple objects (later sources overwrite)', () => { + const defaults = { theme: 'light', fontSize: 14 } + const userPrefs = { theme: 'dark' } + const session = { fontSize: 18 } + + const settings = Object.assign({}, defaults, userPrefs, session) + + expect(settings.theme).toBe('dark') + expect(settings.fontSize).toBe(18) + }) + + it('should perform shallow copy only', () => { + const original = { + name: 'Alice', + scores: [90, 85] + } + + const clone = Object.assign({}, original) + + // Primitive is copied by value + clone.name = 'Bob' + expect(original.name).toBe('Alice') + + // Array is copied by reference + clone.scores.push(100) + expect(original.scores).toEqual([90, 85, 100]) // Modified! + }) + + it('should not copy inherited or non-enumerable properties', () => { + const proto = { inherited: 'from prototype' } + const source = Object.create(proto) + source.own = 'my own property' + + Object.defineProperty(source, 'hidden', { + value: 'non-enumerable', + enumerable: false + }) + + const target = {} + Object.assign(target, source) + + expect(target.own).toBe('my own property') + expect(target.inherited).toBeUndefined() // Not copied + expect(target.hidden).toBeUndefined() // Not copied + }) + }) + + describe('Prototype inspection', () => { + it('Object.getPrototypeOf should return the prototype', () => { + const proto = { test: true } + const obj = Object.create(proto) + + expect(Object.getPrototypeOf(obj)).toBe(proto) + }) + + it('Object.setPrototypeOf should change the prototype', () => { + const swimmer = { swim: () => 'swimming' } + const flyer = { fly: () => 'flying' } + + const duck = { name: 'Donald' } + Object.setPrototypeOf(duck, swimmer) + + expect(duck.swim()).toBe('swimming') + + Object.setPrototypeOf(duck, flyer) + expect(duck.fly()).toBe('flying') + expect(duck.swim).toBeUndefined() + }) + + it('instanceof should check the prototype chain', () => { + function Animal(name) { + this.name = name + } + function Dog(name) { + Animal.call(this, name) + } + Dog.prototype = Object.create(Animal.prototype) + Dog.prototype.constructor = Dog + + const rex = new Dog('Rex') + + expect(rex instanceof Dog).toBe(true) + expect(rex instanceof Animal).toBe(true) + expect(rex instanceof Object).toBe(true) + expect(rex instanceof Array).toBe(false) + }) + + it('isPrototypeOf should check if object is in prototype chain', () => { + const animal = { eats: true } + const dog = Object.create(animal) + + expect(animal.isPrototypeOf(dog)).toBe(true) + expect(Object.prototype.isPrototypeOf(dog)).toBe(true) + expect(Array.prototype.isPrototypeOf(dog)).toBe(false) + }) + }) + + describe('Common prototype methods', () => { + it('hasOwnProperty should check only own properties', () => { + const proto = { inherited: true } + const obj = Object.create(proto) + obj.own = true + + expect(obj.hasOwnProperty('own')).toBe(true) + expect(obj.hasOwnProperty('inherited')).toBe(false) + + // 'in' checks the whole chain + expect('own' in obj).toBe(true) + expect('inherited' in obj).toBe(true) + }) + + it('Object.keys should return only own enumerable properties', () => { + const proto = { inherited: 'value' } + const obj = Object.create(proto) + obj.own1 = 'a' + obj.own2 = 'b' + + expect(Object.keys(obj)).toEqual(['own1', 'own2']) + expect(Object.keys(obj)).not.toContain('inherited') + }) + + it('Object.getOwnPropertyNames should return all own properties', () => { + const obj = { visible: true } + Object.defineProperty(obj, 'hidden', { + value: 'secret', + enumerable: false + }) + + expect(Object.keys(obj)).toEqual(['visible']) + expect(Object.getOwnPropertyNames(obj)).toEqual(['visible', 'hidden']) + }) + }) + + describe('Common mistakes', () => { + it('should not share reference types on prototype', () => { + // Wrong way - array on prototype is shared + function BadPlayer(name) { + this.name = name + } + BadPlayer.prototype.inventory = [] + + const alice = new BadPlayer('Alice') + const bob = new BadPlayer('Bob') + + alice.inventory.push('sword') + expect(bob.inventory).toContain('sword') // Bob has Alice's sword! + + // Correct way - array in constructor + function GoodPlayer(name) { + this.name = name + this.inventory = [] + } + + const charlie = new GoodPlayer('Charlie') + const dave = new GoodPlayer('Dave') + + charlie.inventory.push('shield') + expect(dave.inventory).not.toContain('shield') // Dave's inventory is separate + }) + }) +}) diff --git a/tests/object-oriented/this-call-apply-bind/this-call-apply-bind.test.js b/tests/object-oriented/this-call-apply-bind/this-call-apply-bind.test.js new file mode 100644 index 00000000..de1a107e --- /dev/null +++ b/tests/object-oriented/this-call-apply-bind/this-call-apply-bind.test.js @@ -0,0 +1,1354 @@ +import { describe, it, expect } from 'vitest' + +describe('this, call, apply and bind', () => { + + describe('Documentation Examples', () => { + describe('Introduction: The Pronoun I Analogy', () => { + it('should demonstrate this referring to different objects', () => { + const alice = { + name: "Alice", + introduce() { + return "I am " + this.name + } + } + + const bob = { + name: "Bob", + introduce() { + return "I am " + this.name + } + } + + expect(alice.introduce()).toBe("I am Alice") + expect(bob.introduce()).toBe("I am Bob") + }) + + it('should allow borrowing methods with call (ventriloquist analogy)', () => { + const alice = { name: "Alice" } + const bob = { + name: "Bob", + introduce() { + return "I am " + this.name + } + } + + // Alice borrows Bob's voice + expect(bob.introduce.call(alice)).toBe("I am Alice") + }) + }) + + describe('Dynamic Binding: Call-Time Determination', () => { + it('should have different this values depending on how function is called', () => { + function showThis() { + return this + } + + const obj = { showThis } + + // Plain call - default binding (undefined in strict mode) + expect(showThis()).toBeUndefined() + + // Method call - implicit binding + expect(obj.showThis()).toBe(obj) + + // Explicit binding + const customObj = { name: 'custom' } + expect(showThis.call(customObj)).toBe(customObj) + }) + + it('should allow one function to work with many objects', () => { + function greet() { + return `Hello, I'm ${this.name}!` + } + + const alice = { name: "Alice", greet } + const bob = { name: "Bob", greet } + const charlie = { name: "Charlie", greet } + + expect(alice.greet()).toBe("Hello, I'm Alice!") + expect(bob.greet()).toBe("Hello, I'm Bob!") + expect(charlie.greet()).toBe("Hello, I'm Charlie!") + }) + }) + + describe('Rectangle Class Example (ES6 Classes)', () => { + it('should bind this to instance in class methods', () => { + class Rectangle { + constructor(width, height) { + this.width = width + this.height = height + } + + getArea() { + return this.width * this.height + } + } + + const rect = new Rectangle(10, 5) + expect(rect.getArea()).toBe(50) + }) + }) + + describe('Explicit Binding: introduce() Example', () => { + it('should set this explicitly with call', () => { + function introduce() { + return `I'm ${this.name}, a ${this.role}` + } + + const alice = { name: "Alice", role: "developer" } + const bob = { name: "Bob", role: "designer" } + + expect(introduce.call(alice)).toBe("I'm Alice, a developer") + expect(introduce.call(bob)).toBe("I'm Bob, a designer") + }) + }) + + describe('Partial Application: greet with sayHello/sayGoodbye', () => { + it('should create specialized greeting functions', () => { + function greet(greeting, name) { + return `${greeting}, ${name}!` + } + + const sayHello = greet.bind(null, "Hello") + const sayGoodbye = greet.bind(null, "Goodbye") + + expect(sayHello("Alice")).toBe("Hello, Alice!") + expect(sayHello("Bob")).toBe("Hello, Bob!") + expect(sayGoodbye("Alice")).toBe("Goodbye, Alice!") + }) + }) + }) + + describe('The 5 Binding Rules', () => { + + describe('Rule 1: new Binding', () => { + it('should bind this to new object with constructor function', () => { + function Person(name) { + this.name = name + } + + const alice = new Person('Alice') + expect(alice.name).toBe('Alice') + }) + + it('should bind this to new object with ES6 class', () => { + class Person { + constructor(name) { + this.name = name + } + } + + const bob = new Person('Bob') + expect(bob.name).toBe('Bob') + }) + + it('should create separate instances with their own this', () => { + class Counter { + constructor() { + this.count = 0 + } + increment() { + this.count++ + } + } + + const counter1 = new Counter() + const counter2 = new Counter() + + counter1.increment() + counter1.increment() + counter2.increment() + + expect(counter1.count).toBe(2) + expect(counter2.count).toBe(1) + }) + + it('should allow this to reference instance methods', () => { + class Calculator { + constructor(value) { + this.value = value + } + add(n) { + this.value += n + return this + } + multiply(n) { + this.value *= n + return this + } + } + + const calc = new Calculator(5) + calc.add(3).multiply(2) + + expect(calc.value).toBe(16) + }) + + it('should return the new object unless function returns an object', () => { + function ReturnsNothing(name) { + this.name = name + } + + function ReturnsObject(name) { + this.name = name + return { customName: 'Custom' } + } + + function ReturnsPrimitive(name) { + this.name = name + return 42 // Primitive return is ignored + } + + const obj1 = new ReturnsNothing('Alice') + const obj2 = new ReturnsObject('Bob') + const obj3 = new ReturnsPrimitive('Charlie') + + expect(obj1.name).toBe('Alice') + expect(obj2.customName).toBe('Custom') + expect(obj2.name).toBeUndefined() + expect(obj3.name).toBe('Charlie') // Primitive ignored + }) + + it('should set up prototype chain correctly', () => { + class Animal { + speak() { + return 'Some sound' + } + } + + class Dog extends Animal { + speak() { + return 'Woof!' + } + } + + const dog = new Dog() + expect(dog.speak()).toBe('Woof!') + expect(dog instanceof Dog).toBe(true) + expect(dog instanceof Animal).toBe(true) + }) + + it('should have new binding override explicit binding', () => { + function Person(name) { + this.name = name + } + + const boundPerson = Person.bind({ name: 'Bound' }) + const alice = new boundPerson('Alice') + + // new overrides bind + expect(alice.name).toBe('Alice') + }) + }) + + describe('Rule 2: Explicit Binding (call/apply/bind)', () => { + it('should set this with call()', () => { + function greet() { + return `Hello, ${this.name}` + } + + const alice = { name: 'Alice' } + expect(greet.call(alice)).toBe('Hello, Alice') + }) + + it('should set this with apply()', () => { + function greet() { + return `Hello, ${this.name}` + } + + const bob = { name: 'Bob' } + expect(greet.apply(bob)).toBe('Hello, Bob') + }) + + it('should set this with bind()', () => { + function greet() { + return `Hello, ${this.name}` + } + + const charlie = { name: 'Charlie' } + const boundGreet = greet.bind(charlie) + expect(boundGreet()).toBe('Hello, Charlie') + }) + + it('should have explicit binding override implicit binding', () => { + const alice = { + name: 'Alice', + greet() { + return `Hi, I'm ${this.name}` + } + } + + const bob = { name: 'Bob' } + + // Even though called on alice, we force this to be bob + expect(alice.greet.call(bob)).toBe("Hi, I'm Bob") + }) + + it('should handle null/undefined thisArg in strict mode', () => { + function getThis() { + return this + } + + // In strict mode with null/undefined, this remains null/undefined + expect(getThis.call(null)).toBe(null) + expect(getThis.call(undefined)).toBe(undefined) + }) + }) + + describe('Rule 3: Implicit Binding (Method Call)', () => { + it('should bind this to the object before the dot', () => { + const user = { + name: 'Alice', + getName() { + return this.name + } + } + + expect(user.getName()).toBe('Alice') + }) + + it('should use the immediate object for nested objects', () => { + const company = { + name: 'TechCorp', + department: { + name: 'Engineering', + getName() { + return this.name + } + } + } + + // this is department, not company + expect(company.department.getName()).toBe('Engineering') + }) + + it('should allow method chaining with this', () => { + const calculator = { + value: 0, + add(n) { + this.value += n + return this + }, + subtract(n) { + this.value -= n + return this + }, + getResult() { + return this.value + } + } + + const result = calculator.add(10).subtract(3).add(5).getResult() + expect(result).toBe(12) + }) + + it('should lose implicit binding when method is extracted', () => { + const user = { + name: 'Alice', + getName() { + return this?.name + } + } + + const getName = user.getName + // Lost binding - this is undefined in strict mode + expect(getName()).toBeUndefined() + }) + + it('should lose implicit binding in callbacks', () => { + const user = { + name: 'Alice', + getName() { + return this?.name + } + } + + function executeCallback(callback) { + return callback() + } + + // Passing method as callback loses binding + expect(executeCallback(user.getName)).toBeUndefined() + }) + + it('should work with computed property access', () => { + const obj = { + name: 'Object', + method() { + return this.name + } + } + + const methodName = 'method' + expect(obj[methodName]()).toBe('Object') + }) + }) + + describe('Rule 4: Default Binding', () => { + it('should have undefined this in strict mode for plain function calls', () => { + function getThis() { + return this + } + + // Vitest runs in strict mode + expect(getThis()).toBeUndefined() + }) + + it('should have undefined this in IIFE in strict mode', () => { + const result = (function() { + return this + })() + + expect(result).toBeUndefined() + }) + + it('should have undefined this in nested function calls', () => { + const obj = { + name: 'Object', + method() { + function inner() { + return this + } + return inner() + } + } + + // Inner function uses default binding + expect(obj.method()).toBeUndefined() + }) + }) + + describe('Rule 5: Arrow Functions (Lexical this)', () => { + it('should inherit this from enclosing scope', () => { + const obj = { + name: 'Object', + method() { + const arrow = () => this.name + return arrow() + } + } + + expect(obj.method()).toBe('Object') + }) + + it('should not change this with call/apply/bind on arrow functions', () => { + const obj = { + name: 'Original', + getArrow() { + return () => this.name + } + } + + const arrow = obj.getArrow() + const other = { name: 'Other' } + + // Arrow function ignores explicit binding + expect(arrow()).toBe('Original') + expect(arrow.call(other)).toBe('Original') + expect(arrow.apply(other)).toBe('Original') + expect(arrow.bind(other)()).toBe('Original') + }) + + it('should preserve this in callbacks with arrow functions', () => { + class Counter { + constructor() { + this.count = 0 + } + + incrementWithArrow() { + [1, 2, 3].forEach(() => { + this.count++ + }) + } + } + + const counter = new Counter() + counter.incrementWithArrow() + + expect(counter.count).toBe(3) + }) + + it('should work with arrow function class fields', () => { + class Button { + constructor(label) { + this.label = label + } + + // Arrow function as class field + handleClick = () => { + return `Clicked: ${this.label}` + } + } + + const btn = new Button('Submit') + const handler = btn.handleClick // Extract method + + // Still works because arrow binds lexically + expect(handler()).toBe('Clicked: Submit') + }) + + it('should not have arrow functions work as object methods', () => { + const user = { + name: 'Alice', + // Arrow function as method - BAD! + greet: () => { + return this?.name + } + } + + // this is not user, it's the enclosing scope (undefined in strict mode) + expect(user.greet()).toBeUndefined() + }) + + it('should capture this at definition time, not call time', () => { + function createArrow() { + return () => this + } + + const obj1 = { name: 'obj1' } + const obj2 = { name: 'obj2' } + + // Arrow is created with obj1 as this + const arrow = createArrow.call(obj1) + + // Calling with obj2 doesn't change anything + expect(arrow.call(obj2)).toBe(obj1) + }) + }) + + describe('Arrow Function Limitations', () => { + it('should throw when using arrow function with new', () => { + const ArrowClass = () => {} + + expect(() => { + new ArrowClass() + }).toThrow(TypeError) + }) + + it('should not have arguments object in arrow functions', () => { + // Arrow functions don't have their own arguments + // They would reference arguments from enclosing scope + const arrowWithRest = (...args) => { + return args + } + + expect(arrowWithRest(1, 2, 3)).toEqual([1, 2, 3]) + }) + + it('should demonstrate regular vs arrow in nested context', () => { + const obj = { + name: "Object", + + regularMethod: function() { + // Nested regular function - loses 'this' + function inner() { + return this + } + return inner() + }, + + arrowMethod: function() { + // Nested arrow function - keeps 'this' + const innerArrow = () => { + return this.name + } + return innerArrow() + } + } + + expect(obj.regularMethod()).toBeUndefined() + expect(obj.arrowMethod()).toBe("Object") + }) + }) + }) + + describe('call() Method', () => { + it('should invoke function immediately with specified this', () => { + function greet() { + return `Hello, ${this.name}` + } + + expect(greet.call({ name: 'World' })).toBe('Hello, World') + }) + + it('should pass arguments individually', () => { + function introduce(greeting, punctuation) { + return `${greeting}, I'm ${this.name}${punctuation}` + } + + const alice = { name: 'Alice' } + expect(introduce.call(alice, 'Hi', '!')).toBe("Hi, I'm Alice!") + }) + + it('should allow method borrowing', () => { + const arrayLike = { 0: 'a', 1: 'b', 2: 'c', length: 3 } + + const result = Array.prototype.slice.call(arrayLike) + expect(result).toEqual(['a', 'b', 'c']) + }) + + it('should allow borrowing join method', () => { + const arrayLike = { 0: 'a', 1: 'b', 2: 'c', length: 3 } + + const result = Array.prototype.join.call(arrayLike, '-') + expect(result).toBe('a-b-c') + }) + + it('should work with prototype methods', () => { + const obj = { + 0: 1, + 1: 2, + 2: 3, + length: 3 + } + + const sum = Array.prototype.reduce.call(obj, (acc, val) => acc + val, 0) + expect(sum).toBe(6) + }) + + it('should allow calling parent class methods', () => { + class Animal { + speak() { + return `${this.name} makes a sound` + } + } + + class Dog extends Animal { + constructor(name) { + super() + this.name = name + } + speak() { + const parentSays = Animal.prototype.speak.call(this) + return `${parentSays}. Woof!` + } + } + + const dog = new Dog('Rex') + expect(dog.speak()).toBe('Rex makes a sound. Woof!') + }) + + it('should work with no arguments after thisArg', () => { + function getThisName() { + return this.name + } + + expect(getThisName.call({ name: 'Test' })).toBe('Test') + }) + + it('should keep primitives as-is in strict mode when passed as thisArg', () => { + function getThis() { + return this + } + + // In strict mode, primitives are NOT converted to wrapper objects + const result = getThis.call(42) + expect(result).toBe(42) + expect(typeof result).toBe('number') + }) + }) + + describe('apply() Method', () => { + it('should invoke function immediately with specified this', () => { + function greet() { + return `Hello, ${this.name}` + } + + expect(greet.apply({ name: 'World' })).toBe('Hello, World') + }) + + it('should pass arguments as an array', () => { + function introduce(greeting, punctuation) { + return `${greeting}, I'm ${this.name}${punctuation}` + } + + const bob = { name: 'Bob' } + expect(introduce.apply(bob, ['Hey', '?'])).toBe("Hey, I'm Bob?") + }) + + it('should work with Math.max', () => { + const numbers = [5, 2, 9, 1, 7] + + const max = Math.max.apply(null, numbers) + expect(max).toBe(9) + }) + + it('should work with Math.min', () => { + const numbers = [5, 2, 9, 1, 7] + + const min = Math.min.apply(null, numbers) + expect(min).toBe(1) + }) + + it('should work with array-like arguments', () => { + function sum() { + return Array.prototype.reduce.call(arguments, (acc, n) => acc + n, 0) + } + + const numbers = [1, 2, 3, 4, 5] + expect(sum.apply(null, numbers)).toBe(15) + }) + + it('should work with empty array', () => { + function returnArgs() { + return Array.prototype.slice.call(arguments) + } + + expect(returnArgs.apply(null, [])).toEqual([]) + }) + + it('should work with null/undefined args array', () => { + function noArgs() { + return 'called' + } + + expect(noArgs.apply(null, null)).toBe('called') + expect(noArgs.apply(null, undefined)).toBe('called') + }) + + it('should allow combining arrays with concat-like behavior', () => { + const arr1 = [1, 2, 3] + const arr2 = [4, 5, 6] + + Array.prototype.push.apply(arr1, arr2) + expect(arr1).toEqual([1, 2, 3, 4, 5, 6]) + }) + + it('should be replaceable by spread operator for Math operations', () => { + const numbers = [5, 2, 9, 1, 7] + + // Old way with apply + const maxApply = Math.max.apply(null, numbers) + const minApply = Math.min.apply(null, numbers) + + // Modern way with spread + const maxSpread = Math.max(...numbers) + const minSpread = Math.min(...numbers) + + expect(maxApply).toBe(maxSpread) + expect(minApply).toBe(minSpread) + expect(maxSpread).toBe(9) + expect(minSpread).toBe(1) + }) + }) + + describe('bind() Method', () => { + it('should return a new function', () => { + function greet() { + return `Hello, ${this.name}` + } + + const alice = { name: 'Alice' } + const boundGreet = greet.bind(alice) + + expect(typeof boundGreet).toBe('function') + expect(boundGreet).not.toBe(greet) + }) + + it('should not invoke the function immediately', () => { + let called = false + function setFlag() { + called = true + } + + const bound = setFlag.bind({}) + expect(called).toBe(false) + + bound() + expect(called).toBe(true) + }) + + it('should permanently bind this', () => { + function getName() { + return this.name + } + + const alice = { name: 'Alice' } + const bob = { name: 'Bob' } + + const boundToAlice = getName.bind(alice) + + expect(boundToAlice()).toBe('Alice') + expect(boundToAlice.call(bob)).toBe('Alice') // call ignored + expect(boundToAlice.apply(bob)).toBe('Alice') // apply ignored + }) + + it('should not allow rebinding with another bind', () => { + function getName() { + return this.name + } + + const alice = { name: 'Alice' } + const bob = { name: 'Bob' } + + const boundToAlice = getName.bind(alice) + const triedRebind = boundToAlice.bind(bob) + + expect(triedRebind()).toBe('Alice') // Still Alice! + }) + + it('should support partial application', () => { + function multiply(a, b) { + return a * b + } + + const double = multiply.bind(null, 2) + const triple = multiply.bind(null, 3) + + expect(double(5)).toBe(10) + expect(triple(5)).toBe(15) + }) + + it('should support partial application with multiple arguments', () => { + function greet(greeting, punctuation, name) { + return `${greeting}, ${name}${punctuation}` + } + + const sayHello = greet.bind(null, 'Hello', '!') + + expect(sayHello('Alice')).toBe('Hello, Alice!') + expect(sayHello('Bob')).toBe('Hello, Bob!') + }) + + it('should work with event handler pattern', () => { + class Button { + constructor(label) { + this.label = label + this.handleClick = this.handleClick.bind(this) + } + + handleClick() { + return `${this.label} clicked` + } + } + + const btn = new Button('Submit') + const handler = btn.handleClick + + expect(handler()).toBe('Submit clicked') + }) + + it('should work with setTimeout pattern', () => { + class Delayed { + constructor(message) { + this.message = message + } + + getMessage() { + return this.message + } + } + + const delayed = new Delayed('Hello') + const boundGetMessage = delayed.getMessage.bind(delayed) + + // Simulating what setTimeout would do + const callback = boundGetMessage + expect(callback()).toBe('Hello') + }) + + it('should preserve the length property minus bound args', () => { + function fn(a, b, c) { + return a + b + c + } + + const bound0 = fn.bind(null) + const bound1 = fn.bind(null, 1) + const bound2 = fn.bind(null, 1, 2) + + expect(fn.length).toBe(3) + expect(bound0.length).toBe(3) + expect(bound1.length).toBe(2) + expect(bound2.length).toBe(1) + }) + + it('should work with new even when bound', () => { + function Person(name) { + this.name = name + } + + const BoundPerson = Person.bind({ name: 'Ignored' }) + const alice = new BoundPerson('Alice') + + // new overrides the bound this + expect(alice.name).toBe('Alice') + }) + }) + + describe('Common Patterns', () => { + describe('Method Borrowing', () => { + it('should borrow array methods for array-like objects', () => { + const arrayLike = { + 0: 'first', + 1: 'second', + 2: 'third', + length: 3 + } + + const mapped = Array.prototype.map.call(arrayLike, item => item.toUpperCase()) + expect(mapped).toEqual(['FIRST', 'SECOND', 'THIRD']) + }) + + it('should borrow methods between similar objects', () => { + const logger = { + prefix: '[LOG]', + log(message) { + return `${this.prefix} ${message}` + } + } + + const errorLogger = { + prefix: '[ERROR]' + } + + expect(logger.log.call(errorLogger, 'Something failed')).toBe('[ERROR] Something failed') + }) + + it('should use hasOwnProperty safely', () => { + const obj = Object.create(null) // No prototype + obj.name = 'test' + + // obj.hasOwnProperty would fail, but we can borrow it + const hasOwn = Object.prototype.hasOwnProperty.call(obj, 'name') + expect(hasOwn).toBe(true) + }) + }) + + describe('Partial Application', () => { + it('should create specialized functions', () => { + function log(level, timestamp, message) { + return `[${level}] ${timestamp}: ${message}` + } + + const logError = log.bind(null, 'ERROR') + const logInfo = log.bind(null, 'INFO') + + expect(logError('2024-01-15', 'Failed')).toBe('[ERROR] 2024-01-15: Failed') + expect(logInfo('2024-01-15', 'Started')).toBe('[INFO] 2024-01-15: Started') + }) + + it('should allow creating multiplier functions', () => { + function multiply(a, b) { + return a * b + } + + const double = multiply.bind(null, 2) + const triple = multiply.bind(null, 3) + const quadruple = multiply.bind(null, 4) + + expect(double(10)).toBe(20) + expect(triple(10)).toBe(30) + expect(quadruple(10)).toBe(40) + }) + + it('should work with more complex functions', () => { + function createUrl(protocol, domain, path) { + return `${protocol}://${domain}${path}` + } + + const httpUrl = createUrl.bind(null, 'https') + const apiUrl = httpUrl.bind(null, 'api.example.com') + + expect(apiUrl('/users')).toBe('https://api.example.com/users') + expect(apiUrl('/posts')).toBe('https://api.example.com/posts') + }) + }) + + describe('Preserving Context in Classes', () => { + it('should preserve context with bind in constructor', () => { + class Timer { + constructor() { + this.seconds = 0 + this.tick = this.tick.bind(this) + } + + tick() { + this.seconds++ + return this.seconds + } + } + + const timer = new Timer() + const tick = timer.tick + + expect(tick()).toBe(1) + expect(tick()).toBe(2) + }) + + it('should preserve context with arrow class fields', () => { + class Timer { + seconds = 0 + + tick = () => { + this.seconds++ + return this.seconds + } + } + + const timer = new Timer() + const tick = timer.tick + + expect(tick()).toBe(1) + expect(tick()).toBe(2) + }) + }) + }) + + describe('Gotchas and Edge Cases', () => { + describe('Lost Context Scenarios', () => { + it('should demonstrate lost context in forEach without arrow', () => { + const calculator = { + value: 10, + addAll(numbers) { + const self = this // Old-school workaround + numbers.forEach(function(n) { + self.value += n + }) + return this.value + } + } + + expect(calculator.addAll([1, 2, 3])).toBe(16) + }) + + it('should fix lost context with arrow function', () => { + const calculator = { + value: 10, + addAll(numbers) { + numbers.forEach((n) => { + this.value += n + }) + return this.value + } + } + + expect(calculator.addAll([1, 2, 3])).toBe(16) + }) + + it('should fix lost context with thisArg parameter', () => { + const calculator = { + value: 10, + addAll(numbers) { + numbers.forEach(function(n) { + this.value += n + }, this) // Pass this as second argument + return this.value + } + } + + expect(calculator.addAll([1, 2, 3])).toBe(16) + }) + }) + + describe('this in Different Contexts', () => { + it('should have correct this in nested methods', () => { + const outer = { + name: 'Outer', + inner: { + name: 'Inner', + getOuterName() { + // Can't access outer.name via this + return this.name + } + } + } + + expect(outer.inner.getOuterName()).toBe('Inner') + }) + + it('should demonstrate closure workaround for nested this', () => { + const outer = { + name: 'Outer', + createInner() { + const outerThis = this + return { + name: 'Inner', + getOuterName() { + return outerThis.name + } + } + } + } + + const inner = outer.createInner() + expect(inner.getOuterName()).toBe('Outer') + }) + }) + + describe('Binding Priority', () => { + it('should have new override bind', () => { + function Foo(value) { + this.value = value + } + + const BoundFoo = Foo.bind({ value: 'bound' }) + const instance = new BoundFoo('new') + + expect(instance.value).toBe('new') + }) + + it('should have explicit override implicit', () => { + const obj1 = { + name: 'obj1', + getName() { + return this.name + } + } + + const obj2 = { name: 'obj2' } + + expect(obj1.getName()).toBe('obj1') + expect(obj1.getName.call(obj2)).toBe('obj2') + }) + + it('should have implicit override default', () => { + function getName() { + return this?.name + } + + const obj = { name: 'obj', getName } + + expect(getName()).toBeUndefined() // Default binding + expect(obj.getName()).toBe('obj') // Implicit binding + }) + }) + }) + + describe('Quiz Questions from Documentation', () => { + it('Question 1: extracted method loses context', () => { + const user = { + name: 'Alice', + greet() { + return `Hi, I'm ${this?.name}` + } + } + + const greet = user.greet + expect(greet()).toBe("Hi, I'm undefined") + }) + + it('Question 2: arrow function class fields preserve context', () => { + class Counter { + count = 0 + + increment = () => { + this.count++ + } + } + + const counter = new Counter() + const inc = counter.increment + inc() + inc() + + expect(counter.count).toBe(2) + }) + + it('Question 3: bind cannot be overridden by call', () => { + function greet() { + return `Hello, ${this.name}!` + } + + const alice = { name: 'Alice' } + const bob = { name: 'Bob' } + + const greetAlice = greet.bind(alice) + expect(greetAlice.call(bob)).toBe('Hello, Alice!') + }) + + it('Question 4: nested object uses immediate parent as this', () => { + const obj = { + name: 'Outer', + inner: { + name: 'Inner', + getName() { + return this.name + } + } + } + + expect(obj.inner.getName()).toBe('Inner') + }) + + it('Question 5: forEach callback loses this context', () => { + const calculator = { + value: 10, + add(numbers) { + // This demonstrates the BROKEN behavior + let localValue = this.value + numbers.forEach(function(n) { + // this.value would be undefined here in strict mode + // so we can't actually add to it + localValue += 0 // simulating the broken behavior + }) + return this.value // returns original value unchanged + } + } + + // The value stays 10 because the callback can't access this.value + expect(calculator.add([1, 2, 3])).toBe(10) + }) + + it('Question 5 fixed: forEach with arrow function preserves this', () => { + const calculator = { + value: 10, + add(numbers) { + numbers.forEach((n) => { + this.value += n // Arrow function preserves this + }) + return this.value + } + } + + expect(calculator.add([1, 2, 3])).toBe(16) + }) + + it('Question 6: bind partial application and length property', () => { + function multiply(a, b) { + return a * b + } + + const double = multiply.bind(null, 2) + + expect(double(5)).toBe(10) + expect(double.length).toBe(1) // multiply has 2 params, we pre-filled 1 + }) + }) + + describe('Additional Documentation Examples', () => { + describe('simulateNew function', () => { + it('should simulate new keyword behavior', () => { + function simulateNew(Constructor, ...args) { + // Step 1: Create empty object + const newObject = {} + + // Step 2: Link prototype if it's an object + if (Constructor.prototype !== null && typeof Constructor.prototype === 'object') { + Object.setPrototypeOf(newObject, Constructor.prototype) + } + + // Step 3: Bind this and execute + const result = Constructor.apply(newObject, args) + + // Step 4: Return object (unless constructor returns a non-primitive) + return result !== null && typeof result === 'object' ? result : newObject + } + + function Person(name) { + this.name = name + } + Person.prototype.greet = function() { + return `Hi, I'm ${this.name}` + } + + const alice1 = new Person("Alice") + const alice2 = simulateNew(Person, "Alice") + + expect(alice1.name).toBe("Alice") + expect(alice2.name).toBe("Alice") + expect(alice1.greet()).toBe("Hi, I'm Alice") + expect(alice2.greet()).toBe("Hi, I'm Alice") + expect(alice2 instanceof Person).toBe(true) + }) + + it('should return custom object if constructor returns one', () => { + function simulateNew(Constructor, ...args) { + const newObject = {} + if (Constructor.prototype !== null && typeof Constructor.prototype === 'object') { + Object.setPrototypeOf(newObject, Constructor.prototype) + } + const result = Constructor.apply(newObject, args) + return result !== null && typeof result === 'object' ? result : newObject + } + + function ReturnsObject() { + this.name = "ignored" + return { custom: "object" } + } + + const obj = simulateNew(ReturnsObject) + expect(obj.custom).toBe("object") + expect(obj.name).toBeUndefined() + }) + + it('should handle constructor with non-object prototype', () => { + function simulateNew(Constructor, ...args) { + const newObject = {} + if (Constructor.prototype !== null && typeof Constructor.prototype === 'object') { + Object.setPrototypeOf(newObject, Constructor.prototype) + } + const result = Constructor.apply(newObject, args) + return result !== null && typeof result === 'object' ? result : newObject + } + + function WeirdConstructor(value) { + this.value = value + } + // Set prototype to a primitive (edge case) + WeirdConstructor.prototype = null + + const obj = simulateNew(WeirdConstructor, 42) + expect(obj.value).toBe(42) + // When prototype is null, object keeps Object.prototype + expect(Object.getPrototypeOf(obj)).toBe(Object.prototype) + }) + }) + + describe('apply with args array', () => { + it('should work with introduce function and args array', () => { + function introduce(greeting, role, company) { + return `${greeting}! I'm ${this.name}, ${role} at ${company}.` + } + + const alice = { name: "Alice" } + const args = ["Hello", "engineer", "TechCorp"] + + expect(introduce.apply(alice, args)).toBe("Hello! I'm Alice, engineer at TechCorp.") + expect(introduce.call(alice, ...args)).toBe("Hello! I'm Alice, engineer at TechCorp.") + }) + }) + + describe('Countdown class pattern', () => { + it('should preserve this with bind in setInterval pattern', () => { + class Countdown { + constructor(start) { + this.count = start + } + + tick() { + this.count-- + return this.count + } + } + + const countdown = new Countdown(10) + + // Simulate what setInterval would do - extract the method + const boundTick = countdown.tick.bind(countdown) + + expect(boundTick()).toBe(9) + expect(boundTick()).toBe(8) + expect(boundTick()).toBe(7) + expect(countdown.count).toBe(7) + }) + + it('should lose this without bind', () => { + class Countdown { + constructor(start) { + this.count = start + } + + tick() { + return this?.count + } + } + + const countdown = new Countdown(10) + const unboundTick = countdown.tick + + // Without bind, this is undefined + expect(unboundTick()).toBeUndefined() + }) + }) + }) +}) diff --git a/tests/web-platform/dom/dom.test.js b/tests/web-platform/dom/dom.test.js new file mode 100644 index 00000000..5c41802d --- /dev/null +++ b/tests/web-platform/dom/dom.test.js @@ -0,0 +1,1384 @@ +/** + * @vitest-environment jsdom + */ + +import { describe, it, expect, beforeEach } from 'vitest' + +// ============================================================================= +// DOM AND LAYOUT TREES - TEST SUITE +// Tests for code examples from docs/concepts/dom.mdx +// ============================================================================= + +describe('DOM and Layout Trees', () => { + // Reset document body before each test + beforeEach(() => { + document.body.innerHTML = '' + document.head.innerHTML = '' + }) + + // =========================================================================== + // NODE TYPES AND STRUCTURE + // =========================================================================== + describe('Node Types and Structure', () => { + it('should identify element node type', () => { + const div = document.createElement('div') + expect(div.nodeType).toBe(1) + expect(div.nodeType).toBe(Node.ELEMENT_NODE) + expect(div.nodeName).toBe('DIV') + }) + + it('should identify text node type', () => { + const text = document.createTextNode('Hello') + expect(text.nodeType).toBe(3) + expect(text.nodeType).toBe(Node.TEXT_NODE) + expect(text.nodeName).toBe('#text') + }) + + it('should identify comment node type', () => { + const comment = document.createComment('This is a comment') + expect(comment.nodeType).toBe(8) + expect(comment.nodeType).toBe(Node.COMMENT_NODE) + expect(comment.nodeName).toBe('#comment') + }) + + it('should identify document node type', () => { + expect(document.nodeType).toBe(9) + expect(document.nodeType).toBe(Node.DOCUMENT_NODE) + expect(document.nodeName).toBe('#document') + }) + + it('should identify document fragment node type', () => { + const fragment = document.createDocumentFragment() + expect(fragment.nodeType).toBe(11) + expect(fragment.nodeType).toBe(Node.DOCUMENT_FRAGMENT_NODE) + expect(fragment.nodeName).toBe('#document-fragment') + }) + + it('should have correct node type constants', () => { + expect(Node.ELEMENT_NODE).toBe(1) + expect(Node.TEXT_NODE).toBe(3) + expect(Node.COMMENT_NODE).toBe(8) + expect(Node.DOCUMENT_NODE).toBe(9) + expect(Node.DOCUMENT_FRAGMENT_NODE).toBe(11) + }) + + it('should access document properties', () => { + expect(document.documentElement.tagName).toBe('HTML') + expect(document.head).toBeTruthy() + expect(document.body).toBeTruthy() + }) + + it('should be able to set document title', () => { + document.title = 'New Title' + expect(document.title).toBe('New Title') + }) + }) + + // =========================================================================== + // SELECTING ELEMENTS + // =========================================================================== + describe('Selecting Elements', () => { + beforeEach(() => { + document.body.innerHTML = ` + <div id="hero">Welcome!</div> + <p class="intro">First</p> + <p class="intro">Second</p> + <p>Third</p> + <nav> + <a href="#" class="active">Home</a> + <a href="#">About</a> + </nav> + <input type="text" data-id="123"> + ` + }) + + describe('getElementById', () => { + it('should select element by id', () => { + const hero = document.getElementById('hero') + expect(hero).toBeTruthy() + expect(hero.textContent).toBe('Welcome!') + }) + + it('should return null for non-existent id', () => { + const ghost = document.getElementById('nonexistent') + expect(ghost).toBeNull() + }) + }) + + describe('getElementsByClassName', () => { + it('should select elements by class name', () => { + const intros = document.getElementsByClassName('intro') + expect(intros.length).toBe(2) + expect(intros[0].textContent).toBe('First') + }) + + it('should return empty collection for non-existent class', () => { + const ghosts = document.getElementsByClassName('nonexistent') + expect(ghosts.length).toBe(0) + }) + }) + + describe('getElementsByTagName', () => { + it('should select elements by tag name', () => { + const allParagraphs = document.getElementsByTagName('p') + expect(allParagraphs.length).toBe(3) + }) + }) + + describe('querySelector', () => { + it('should select first matching element', () => { + const firstButton = document.querySelector('a') + expect(firstButton.textContent).toBe('Home') + }) + + it('should select by id', () => { + const hero = document.querySelector('#hero') + expect(hero.textContent).toBe('Welcome!') + }) + + it('should select by class', () => { + const firstIntro = document.querySelector('.intro') + expect(firstIntro.textContent).toBe('First') + }) + + it('should select by complex selector', () => { + const navLink = document.querySelector('nav a.active') + expect(navLink.textContent).toBe('Home') + }) + + it('should select by attribute', () => { + const dataItem = document.querySelector('[data-id="123"]') + expect(dataItem.tagName).toBe('INPUT') + }) + + it('should return null for no match', () => { + const ghost = document.querySelector('.nonexistent') + expect(ghost).toBeNull() + }) + }) + + describe('querySelectorAll', () => { + it('should select all matching elements', () => { + const allCards = document.querySelectorAll('.intro') + expect(allCards.length).toBe(2) + }) + + it('should return empty NodeList for no matches', () => { + const ghosts = document.querySelectorAll('.nonexistent') + expect(ghosts.length).toBe(0) + }) + + it('should support complex selectors', () => { + const links = document.querySelectorAll('nav a') + expect(links.length).toBe(2) + }) + }) + + describe('Scoped Selection', () => { + it('should select within a parent element', () => { + const nav = document.querySelector('nav') + const navLinks = nav.querySelectorAll('a') + expect(navLinks.length).toBe(2) + }) + + it('should find specific child in parent', () => { + const nav = document.querySelector('nav') + const activeLink = nav.querySelector('.active') + expect(activeLink.textContent).toBe('Home') + }) + }) + }) + + // =========================================================================== + // LIVE VS STATIC COLLECTIONS + // =========================================================================== + describe('Live vs Static Collections', () => { + beforeEach(() => { + document.body.innerHTML = ` + <div class="item">One</div> + <div class="item">Two</div> + <div class="item">Three</div> + ` + }) + + it('should demonstrate live HTMLCollection updates', () => { + const liveList = document.getElementsByClassName('item') + expect(liveList.length).toBe(3) + + // Add a new item + const newItem = document.createElement('div') + newItem.className = 'item' + document.body.appendChild(newItem) + + // Live collection is automatically updated + expect(liveList.length).toBe(4) + }) + + it('should demonstrate static NodeList does not update', () => { + const staticList = document.querySelectorAll('.item') + expect(staticList.length).toBe(3) + + // Add a new item + const newItem = document.createElement('div') + newItem.className = 'item' + document.body.appendChild(newItem) + + // Static list is still the old snapshot + expect(staticList.length).toBe(3) + }) + }) + + // =========================================================================== + // DOM TRAVERSAL + // =========================================================================== + describe('DOM Traversal', () => { + beforeEach(() => { + document.body.innerHTML = ` + <ul id="list"> + <li id="first">One</li> + <li id="second">Two</li> + <li id="third">Three</li> + </ul> + ` + }) + + describe('Traversing Downwards', () => { + it('should get child nodes including text nodes', () => { + const ul = document.querySelector('ul') + // childNodes includes text nodes from whitespace + expect(ul.childNodes.length).toBeGreaterThan(3) + }) + + it('should get only element children', () => { + const ul = document.querySelector('ul') + expect(ul.children.length).toBe(3) + expect(ul.children[0].textContent).toBe('One') + }) + + it('should get first and last element children', () => { + const ul = document.querySelector('ul') + expect(ul.firstElementChild.textContent).toBe('One') + expect(ul.lastElementChild.textContent).toBe('Three') + }) + + it('should demonstrate firstChild vs firstElementChild', () => { + const ul = document.querySelector('ul') + // firstChild might be a text node (whitespace) + const firstChild = ul.firstChild + const firstElementChild = ul.firstElementChild + + // firstElementChild is always an element + expect(firstElementChild.tagName).toBe('LI') + // firstChild might be text node + expect(firstChild.nodeType === Node.TEXT_NODE || firstChild.nodeType === Node.ELEMENT_NODE).toBe(true) + }) + }) + + describe('Traversing Upwards', () => { + it('should get parent node', () => { + const li = document.querySelector('li') + expect(li.parentNode.tagName).toBe('UL') + }) + + it('should get parent element', () => { + const li = document.querySelector('li') + expect(li.parentElement.tagName).toBe('UL') + }) + + it('should find ancestor with closest()', () => { + const li = document.querySelector('li') + const ul = li.closest('ul') + expect(ul.id).toBe('list') + }) + + it('should return element itself if it matches closest()', () => { + const li = document.querySelector('li') + const self = li.closest('li') + expect(self).toBe(li) + }) + + it('should return null if no ancestor matches closest()', () => { + const li = document.querySelector('li') + const result = li.closest('.nonexistent') + expect(result).toBeNull() + }) + }) + + describe('Traversing Sideways', () => { + it('should get next element sibling', () => { + const first = document.querySelector('#first') + const second = first.nextElementSibling + expect(second.id).toBe('second') + }) + + it('should get previous element sibling', () => { + const second = document.querySelector('#second') + const first = second.previousElementSibling + expect(first.id).toBe('first') + }) + + it('should return null at boundaries', () => { + const first = document.querySelector('#first') + const third = document.querySelector('#third') + expect(first.previousElementSibling).toBeNull() + expect(third.nextElementSibling).toBeNull() + }) + }) + + describe('Building Ancestor Trail', () => { + it('should get all ancestors of an element', () => { + document.body.innerHTML = ` + <main> + <section> + <div class="deeply-nested">Content</div> + </section> + </main> + ` + + function getAncestors(element) { + const ancestors = [] + let current = element.parentElement + + while (current && current !== document.body) { + ancestors.push(current) + current = current.parentElement + } + + return ancestors + } + + const deepElement = document.querySelector('.deeply-nested') + const ancestors = getAncestors(deepElement) + + expect(ancestors.length).toBe(2) + expect(ancestors[0].tagName).toBe('SECTION') + expect(ancestors[1].tagName).toBe('MAIN') + }) + }) + }) + + // =========================================================================== + // CREATING AND MANIPULATING ELEMENTS + // =========================================================================== + describe('Creating and Manipulating Elements', () => { + describe('Creating Elements', () => { + it('should create a new element', () => { + const div = document.createElement('div') + expect(div.tagName).toBe('DIV') + expect(div.parentNode).toBeNull() // Not yet in DOM + }) + + it('should create a text node', () => { + const text = document.createTextNode('Hello, world!') + expect(text.nodeType).toBe(Node.TEXT_NODE) + expect(text.textContent).toBe('Hello, world!') + }) + + it('should create a comment node', () => { + const comment = document.createComment('This is a comment') + expect(comment.nodeType).toBe(Node.COMMENT_NODE) + expect(comment.textContent).toBe('This is a comment') + }) + }) + + describe('appendChild', () => { + it('should add element as last child', () => { + document.body.innerHTML = '<ul><li>Existing</li></ul>' + const ul = document.querySelector('ul') + const li = document.createElement('li') + li.textContent = 'New item' + + ul.appendChild(li) + + expect(ul.lastElementChild.textContent).toBe('New item') + expect(ul.children.length).toBe(2) + }) + }) + + describe('insertBefore', () => { + it('should insert element before reference node', () => { + document.body.innerHTML = '<ul><li>Existing</li></ul>' + const ul = document.querySelector('ul') + const existingLi = ul.querySelector('li') + const newLi = document.createElement('li') + newLi.textContent = 'First!' + + ul.insertBefore(newLi, existingLi) + + expect(ul.firstElementChild.textContent).toBe('First!') + expect(ul.children.length).toBe(2) + }) + }) + + describe('append and prepend', () => { + it('should append multiple items including strings', () => { + const div = document.createElement('div') + const span = document.createElement('span') + + div.append('Text', span, 'More text') + + expect(div.childNodes.length).toBe(3) + expect(div.textContent).toBe('TextMore text') + }) + + it('should prepend element to start', () => { + document.body.innerHTML = '<div>Existing</div>' + const div = document.querySelector('div') + const strong = document.createElement('strong') + strong.textContent = 'New' + + div.prepend(strong) + + expect(div.firstElementChild.tagName).toBe('STRONG') + }) + }) + + describe('before and after', () => { + it('should insert as previous sibling with before()', () => { + document.body.innerHTML = '<h1>Title</h1>' + const h1 = document.querySelector('h1') + const nav = document.createElement('nav') + + h1.before(nav) + + expect(h1.previousElementSibling.tagName).toBe('NAV') + }) + + it('should insert as next sibling with after()', () => { + document.body.innerHTML = '<h1>Title</h1>' + const h1 = document.querySelector('h1') + const p = document.createElement('p') + + h1.after(p) + + expect(h1.nextElementSibling.tagName).toBe('P') + }) + }) + + describe('insertAdjacentHTML', () => { + it('should insert at all four positions', () => { + document.body.innerHTML = '<div id="target">Content</div>' + const div = document.querySelector('#target') + + div.insertAdjacentHTML('beforebegin', '<p id="before">Before</p>') + div.insertAdjacentHTML('afterbegin', '<span id="first">First</span>') + div.insertAdjacentHTML('beforeend', '<span id="last">Last</span>') + div.insertAdjacentHTML('afterend', '<p id="after">After</p>') + + expect(div.previousElementSibling.id).toBe('before') + expect(div.firstElementChild.id).toBe('first') + expect(div.lastElementChild.id).toBe('last') + expect(div.nextElementSibling.id).toBe('after') + }) + }) + + describe('Removing Elements', () => { + it('should remove element with remove()', () => { + document.body.innerHTML = '<div class="to-remove">Gone</div>' + const element = document.querySelector('.to-remove') + + element.remove() + + expect(document.querySelector('.to-remove')).toBeNull() + }) + + it('should remove child with removeChild()', () => { + document.body.innerHTML = '<ul><li>Keep</li><li class="remove">Remove</li></ul>' + const ul = document.querySelector('ul') + const toRemove = ul.querySelector('.remove') + + ul.removeChild(toRemove) + + expect(ul.children.length).toBe(1) + expect(ul.querySelector('.remove')).toBeNull() + }) + }) + + describe('Cloning Elements', () => { + it('should shallow clone element only', () => { + document.body.innerHTML = '<div class="card"><p>Content</p></div>' + const original = document.querySelector('.card') + + const shallow = original.cloneNode(false) + + expect(shallow.className).toBe('card') + expect(shallow.children.length).toBe(0) // No children + }) + + it('should deep clone element with descendants', () => { + document.body.innerHTML = '<div class="card"><p>Content</p></div>' + const original = document.querySelector('.card') + + const deep = original.cloneNode(true) + + expect(deep.className).toBe('card') + expect(deep.children.length).toBe(1) + expect(deep.querySelector('p').textContent).toBe('Content') + }) + + it('should create detached clone', () => { + document.body.innerHTML = '<div class="card">Content</div>' + const original = document.querySelector('.card') + + const clone = original.cloneNode(true) + + expect(clone.parentNode).toBeNull() + }) + }) + + describe('DocumentFragment', () => { + it('should batch add elements with fragment', () => { + document.body.innerHTML = '<ul></ul>' + const ul = document.querySelector('ul') + const fragment = document.createDocumentFragment() + + for (let i = 0; i < 5; i++) { + const li = document.createElement('li') + li.textContent = `Item ${i}` + fragment.appendChild(li) + } + + ul.appendChild(fragment) + + expect(ul.children.length).toBe(5) + expect(ul.firstElementChild.textContent).toBe('Item 0') + expect(ul.lastElementChild.textContent).toBe('Item 4') + }) + + it('should have no parent', () => { + const fragment = document.createDocumentFragment() + expect(fragment.parentNode).toBeNull() + }) + }) + }) + + // =========================================================================== + // MODIFYING CONTENT + // =========================================================================== + describe('Modifying Content', () => { + describe('innerHTML', () => { + it('should read HTML content', () => { + document.body.innerHTML = '<div><p>Hello</p><span>World</span></div>' + const div = document.querySelector('div') + + expect(div.innerHTML).toBe('<p>Hello</p><span>World</span>') + }) + + it('should set HTML content', () => { + document.body.innerHTML = '<div></div>' + const div = document.querySelector('div') + + div.innerHTML = '<h1>New Title</h1><p>New paragraph</p>' + + expect(div.children.length).toBe(2) + expect(div.querySelector('h1').textContent).toBe('New Title') + }) + + it('should clear content with empty string', () => { + document.body.innerHTML = '<div><p>Content</p></div>' + const div = document.querySelector('div') + + div.innerHTML = '' + + expect(div.children.length).toBe(0) + }) + }) + + describe('textContent', () => { + it('should read text content ignoring HTML', () => { + document.body.innerHTML = '<div><p>Hello</p><span>World</span></div>' + const div = document.querySelector('div') + + expect(div.textContent).toBe('HelloWorld') + }) + + it('should set text content escaping HTML', () => { + document.body.innerHTML = '<div></div>' + const div = document.querySelector('div') + + div.textContent = '<script>alert("XSS")</script>' + + // HTML is escaped, not parsed + expect(div.children.length).toBe(0) + expect(div.textContent).toBe('<script>alert("XSS")</script>') + }) + }) + + describe('innerText vs textContent', () => { + it('textContent includes hidden text, innerText may not', () => { + document.body.innerHTML = '<div>Hello <span style="display:none">Hidden</span> World</div>' + const div = document.querySelector('div') + + // textContent includes all text + expect(div.textContent).toContain('Hidden') + + // Note: In jsdom, innerText may behave like textContent + // In real browsers, innerText would exclude display:none text + }) + }) + }) + + // =========================================================================== + // WORKING WITH ATTRIBUTES + // =========================================================================== + describe('Working with Attributes', () => { + describe('Standard Attribute Methods', () => { + it('should get attribute value', () => { + document.body.innerHTML = '<a href="https://example.com" target="_blank">Link</a>' + const link = document.querySelector('a') + + expect(link.getAttribute('href')).toBe('https://example.com') + expect(link.getAttribute('target')).toBe('_blank') + }) + + it('should set attribute value', () => { + document.body.innerHTML = '<a href="#">Link</a>' + const link = document.querySelector('a') + + link.setAttribute('href', 'https://newurl.com') + link.setAttribute('target', '_blank') + + expect(link.getAttribute('href')).toBe('https://newurl.com') + expect(link.getAttribute('target')).toBe('_blank') + }) + + it('should check if attribute exists', () => { + document.body.innerHTML = '<a href="#" target="_blank">Link</a>' + const link = document.querySelector('a') + + expect(link.hasAttribute('target')).toBe(true) + expect(link.hasAttribute('rel')).toBe(false) + }) + + it('should remove attribute', () => { + document.body.innerHTML = '<a href="#" target="_blank">Link</a>' + const link = document.querySelector('a') + + link.removeAttribute('target') + + expect(link.hasAttribute('target')).toBe(false) + }) + }) + + describe('Properties vs Attributes', () => { + it('should show difference between attribute and property', () => { + document.body.innerHTML = '<input type="text" value="initial">' + const input = document.querySelector('input') + + // Both start the same + expect(input.getAttribute('value')).toBe('initial') + expect(input.value).toBe('initial') + + // Change the property (simulating user input) + input.value = 'new text' + + // Attribute stays the same, property changes + expect(input.getAttribute('value')).toBe('initial') + expect(input.value).toBe('new text') + }) + + it('should show checkbox property vs attribute', () => { + document.body.innerHTML = '<input type="checkbox" checked>' + const checkbox = document.querySelector('input') + + // Attribute is string or null + expect(checkbox.getAttribute('checked')).toBe('') + + // Property is boolean + expect(checkbox.checked).toBe(true) + + // Toggle the property + checkbox.checked = false + expect(checkbox.checked).toBe(false) + // Attribute may still exist + }) + }) + + describe('Data Attributes and dataset API', () => { + it('should read data attributes via dataset', () => { + document.body.innerHTML = '<div id="user" data-user-id="123" data-role="admin"></div>' + const user = document.querySelector('#user') + + expect(user.dataset.userId).toBe('123') + expect(user.dataset.role).toBe('admin') + }) + + it('should write data attributes via dataset', () => { + document.body.innerHTML = '<div id="user"></div>' + const user = document.querySelector('#user') + + user.dataset.lastLogin = '2024-01-15' + + expect(user.getAttribute('data-last-login')).toBe('2024-01-15') + }) + + it('should delete data attributes', () => { + document.body.innerHTML = '<div data-role="admin"></div>' + const div = document.querySelector('div') + + delete div.dataset.role + + expect(div.hasAttribute('data-role')).toBe(false) + }) + + it('should check if data attribute exists', () => { + document.body.innerHTML = '<div data-user-id="123"></div>' + const div = document.querySelector('div') + + expect('userId' in div.dataset).toBe(true) + expect('role' in div.dataset).toBe(false) + }) + }) + }) + + // =========================================================================== + // STYLING ELEMENTS + // =========================================================================== + describe('Styling Elements', () => { + describe('style Property', () => { + it('should set inline styles', () => { + document.body.innerHTML = '<div></div>' + const box = document.querySelector('div') + + box.style.backgroundColor = 'blue' + box.style.fontSize = '20px' + + expect(box.style.backgroundColor).toBe('blue') + expect(box.style.fontSize).toBe('20px') + }) + + it('should remove inline style with empty string', () => { + document.body.innerHTML = '<div style="color: red;"></div>' + const box = document.querySelector('div') + + box.style.color = '' + + expect(box.style.color).toBe('') + }) + + it('should set multiple styles with cssText', () => { + document.body.innerHTML = '<div></div>' + const box = document.querySelector('div') + + box.style.cssText = 'background: red; font-size: 16px; padding: 10px;' + + expect(box.style.background).toContain('red') + expect(box.style.fontSize).toBe('16px') + expect(box.style.padding).toBe('10px') + }) + }) + + describe('getComputedStyle', () => { + it('should get computed styles', () => { + document.body.innerHTML = '<div style="display: block;"></div>' + const box = document.querySelector('div') + + const styles = getComputedStyle(box) + + expect(styles.display).toBe('block') + }) + }) + + describe('classList API', () => { + it('should add classes', () => { + document.body.innerHTML = '<button></button>' + const button = document.querySelector('button') + + button.classList.add('active') + button.classList.add('btn', 'btn-primary') + + expect(button.classList.contains('active')).toBe(true) + expect(button.classList.contains('btn')).toBe(true) + expect(button.classList.contains('btn-primary')).toBe(true) + }) + + it('should remove classes', () => { + document.body.innerHTML = '<button class="active btn btn-primary"></button>' + const button = document.querySelector('button') + + button.classList.remove('active') + button.classList.remove('btn', 'btn-primary') + + expect(button.classList.contains('active')).toBe(false) + expect(button.classList.contains('btn')).toBe(false) + }) + + it('should toggle classes', () => { + document.body.innerHTML = '<button></button>' + const button = document.querySelector('button') + + button.classList.toggle('active') + expect(button.classList.contains('active')).toBe(true) + + button.classList.toggle('active') + expect(button.classList.contains('active')).toBe(false) + }) + + it('should toggle with condition', () => { + document.body.innerHTML = '<button></button>' + const button = document.querySelector('button') + + button.classList.toggle('active', true) + expect(button.classList.contains('active')).toBe(true) + + button.classList.toggle('active', false) + expect(button.classList.contains('active')).toBe(false) + }) + + it('should replace class', () => { + document.body.innerHTML = '<button class="btn-primary"></button>' + const button = document.querySelector('button') + + button.classList.replace('btn-primary', 'btn-secondary') + + expect(button.classList.contains('btn-primary')).toBe(false) + expect(button.classList.contains('btn-secondary')).toBe(true) + }) + + it('should get class count', () => { + document.body.innerHTML = '<button class="btn btn-primary active"></button>' + const button = document.querySelector('button') + + expect(button.classList.length).toBe(3) + }) + }) + }) + + // =========================================================================== + // COMMON PATTERNS + // =========================================================================== + describe('Common Patterns', () => { + describe('Checking Element Existence', () => { + it('should check if element exists with querySelector', () => { + document.body.innerHTML = '<div class="exists">Found</div>' + + const element = document.querySelector('.exists') + if (element) { + element.textContent = 'Updated!' + } + + expect(document.querySelector('.exists').textContent).toBe('Updated!') + }) + + it('should handle non-existent element safely', () => { + document.body.innerHTML = '' + + const element = document.querySelector('.maybe-exists') + + // Using optional chaining (no error) + element?.classList.add('active') + + expect(element).toBeNull() + }) + }) + + describe('Event Delegation Pattern', () => { + it('should use closest() for event delegation', () => { + document.body.innerHTML = ` + <div class="card-container"> + <div class="card"> + <button class="btn">Click</button> + </div> + </div> + ` + + let clickedCard = null + const container = document.querySelector('.card-container') + + // Simulate event delegation + const btn = document.querySelector('.btn') + const card = btn.closest('.card') + + if (card) { + clickedCard = card + } + + expect(clickedCard).not.toBeNull() + expect(clickedCard.classList.contains('card')).toBe(true) + }) + }) + + describe('Security Patterns - XSS Prevention', () => { + it('should demonstrate innerHTML vulnerability with script-like content', () => { + document.body.innerHTML = '<div id="output"></div>' + const output = document.getElementById('output') + + // innerHTML can render HTML - potential XSS vector + const maliciousInput = '<img src="x" onerror="alert(1)">' + output.innerHTML = maliciousInput + + // The img tag is actually created + const img = output.querySelector('img') + expect(img).not.toBeNull() + expect(img.getAttribute('onerror')).toBe('alert(1)') + }) + + it('should use textContent to safely render user input', () => { + document.body.innerHTML = '<div id="output"></div>' + const output = document.getElementById('output') + + // textContent escapes HTML - safe from XSS + const maliciousInput = '<img src="x" onerror="alert(1)">' + output.textContent = maliciousInput + + // No img tag created - text is escaped + const img = output.querySelector('img') + expect(img).toBeNull() + expect(output.textContent).toBe('<img src="x" onerror="alert(1)">') + }) + + it('should show difference between innerHTML and textContent with HTML entities', () => { + document.body.innerHTML = '<div id="html-output"></div><div id="text-output"></div>' + + const htmlOutput = document.getElementById('html-output') + const textOutput = document.getElementById('text-output') + + const userInput = '<script>steal(cookies)</script>' + + htmlOutput.innerHTML = userInput + textOutput.textContent = userInput + + // innerHTML parses the HTML (script won't execute in modern browsers but DOM is modified) + expect(htmlOutput.children.length).toBeGreaterThanOrEqual(0) + + // textContent treats it as plain text + expect(textOutput.textContent).toBe('<script>steal(cookies)</script>') + expect(textOutput.children.length).toBe(0) + }) + }) + + describe('Attribute Shortcuts', () => { + it('should access id directly on element', () => { + document.body.innerHTML = '<div id="myElement"></div>' + const element = document.getElementById('myElement') + + expect(element.id).toBe('myElement') + + element.id = 'newId' + expect(element.id).toBe('newId') + expect(document.getElementById('newId')).toBe(element) + }) + + it('should access className directly on element', () => { + document.body.innerHTML = '<div class="box large"></div>' + const element = document.querySelector('.box') + + expect(element.className).toBe('box large') + + element.className = 'container small' + expect(element.className).toBe('container small') + }) + + it('should access href directly on anchor elements', () => { + document.body.innerHTML = '<a href="https://example.com">Link</a>' + const link = document.querySelector('a') + + expect(link.href).toBe('https://example.com/') + + link.href = 'https://test.com' + expect(link.href).toBe('https://test.com/') + }) + + it('should access src directly on image elements', () => { + document.body.innerHTML = '<img src="photo.jpg" alt="Photo">' + const img = document.querySelector('img') + + expect(img.src).toContain('photo.jpg') + + img.src = 'newphoto.png' + expect(img.src).toContain('newphoto.png') + }) + + it('should access title directly on elements', () => { + document.body.innerHTML = '<button title="Click me">Button</button>' + const button = document.querySelector('button') + + expect(button.title).toBe('Click me') + + button.title = 'New tooltip' + expect(button.title).toBe('New tooltip') + }) + }) + + describe('className vs classList Comparison', () => { + it('should replace all classes when using className', () => { + document.body.innerHTML = '<div class="one two three"></div>' + const element = document.querySelector('div') + + // className replaces everything + element.className = 'four' + + expect(element.className).toBe('four') + expect(element.classList.contains('one')).toBe(false) + expect(element.classList.contains('four')).toBe(true) + }) + + it('should add single class without affecting others using classList', () => { + document.body.innerHTML = '<div class="one two three"></div>' + const element = document.querySelector('div') + + // classList.add preserves existing classes + element.classList.add('four') + + expect(element.classList.contains('one')).toBe(true) + expect(element.classList.contains('two')).toBe(true) + expect(element.classList.contains('three')).toBe(true) + expect(element.classList.contains('four')).toBe(true) + }) + + it('should toggle class on and off with classList', () => { + document.body.innerHTML = '<div class="active"></div>' + const element = document.querySelector('div') + + expect(element.classList.contains('active')).toBe(true) + + element.classList.toggle('active') + expect(element.classList.contains('active')).toBe(false) + + element.classList.toggle('active') + expect(element.classList.contains('active')).toBe(true) + }) + }) + + describe('Performance Patterns', () => { + it('should cache DOM references instead of repeated queries', () => { + document.body.innerHTML = '<div id="target">Content</div>' + + // Bad: querying multiple times (we just demonstrate the pattern) + const query1 = document.getElementById('target') + const query2 = document.getElementById('target') + const query3 = document.getElementById('target') + + // Good: cache the reference + const cached = document.getElementById('target') + const ref1 = cached + const ref2 = cached + const ref3 = cached + + // All references point to same element + expect(ref1).toBe(ref2) + expect(ref2).toBe(ref3) + expect(cached).toBe(query1) + }) + + it('should batch DOM updates using documentFragment', () => { + document.body.innerHTML = '<ul id="list"></ul>' + const list = document.getElementById('list') + + // Use fragment to batch insertions + const fragment = document.createDocumentFragment() + + for (let i = 0; i < 5; i++) { + const li = document.createElement('li') + li.textContent = `Item ${i}` + fragment.appendChild(li) + } + + // Single DOM update + list.appendChild(fragment) + + expect(list.children.length).toBe(5) + expect(list.children[0].textContent).toBe('Item 0') + expect(list.children[4].textContent).toBe('Item 4') + }) + + it('should avoid layout thrashing by batching reads and writes', () => { + document.body.innerHTML = ` + <div class="box" style="width: 100px; height: 100px;"></div> + <div class="box" style="width: 100px; height: 100px;"></div> + ` + const boxes = document.querySelectorAll('.box') + + // Good pattern: read all first, then write all + const heights = [] + + // Batch reads - get heights from style (JSDOM doesn't compute offsetHeight) + boxes.forEach(box => { + heights.push(parseInt(box.style.height, 10)) + }) + + // Batch writes + boxes.forEach((box, i) => { + box.style.height = `${heights[i] + 10}px` + }) + + expect(boxes[0].style.height).toBe('110px') + expect(boxes[1].style.height).toBe('110px') + }) + + it('should use textContent for better performance than innerHTML for text', () => { + document.body.innerHTML = '<div id="target"></div>' + const target = document.getElementById('target') + + // textContent is faster for plain text (no HTML parsing) + target.textContent = 'Plain text content' + + expect(target.textContent).toBe('Plain text content') + expect(target.innerHTML).toBe('Plain text content') + expect(target.children.length).toBe(0) + }) + }) + + describe('Properties vs Attributes Extended', () => { + it('should handle maxLength property on input', () => { + document.body.innerHTML = '<input type="text" maxlength="10">' + const input = document.querySelector('input') + + // Property returns number + expect(input.maxLength).toBe(10) + expect(typeof input.maxLength).toBe('number') + + // Attribute returns string + expect(input.getAttribute('maxlength')).toBe('10') + expect(typeof input.getAttribute('maxlength')).toBe('string') + }) + + it('should handle checked property on different input types', () => { + document.body.innerHTML = ` + <input type="checkbox" id="cb" checked> + <input type="radio" id="rb" name="group" checked> + ` + + const checkbox = document.getElementById('cb') + const radio = document.getElementById('rb') + + // Both have boolean checked property + expect(checkbox.checked).toBe(true) + expect(radio.checked).toBe(true) + + // Toggle checkbox + checkbox.checked = false + expect(checkbox.checked).toBe(false) + + // Attribute still shows original + expect(checkbox.hasAttribute('checked')).toBe(true) + }) + }) + + describe('Clone ID Collision Prevention', () => { + it('should demonstrate ID collision issue with cloneNode', () => { + document.body.innerHTML = '<div id="original">Content</div>' + const original = document.getElementById('original') + + // Clone keeps the same ID - causes collision! + const clone = original.cloneNode(true) + document.body.appendChild(clone) + + // Now we have two elements with same ID + const allWithId = document.querySelectorAll('#original') + expect(allWithId.length).toBe(2) + + // getElementById returns only first one + expect(document.getElementById('original')).toBe(original) + }) + + it('should fix ID collision by changing cloned element ID', () => { + document.body.innerHTML = '<div id="original">Content</div>' + const original = document.getElementById('original') + + const clone = original.cloneNode(true) + + // Fix: change ID before appending + clone.id = 'clone-1' + document.body.appendChild(clone) + + // No collision - both accessible + expect(document.getElementById('original')).toBe(original) + expect(document.getElementById('clone-1')).toBe(clone) + expect(document.getElementById('clone-1').textContent).toBe('Content') + }) + }) + + describe('Complex Selectors', () => { + it('should select elements using :not() pseudo-selector', () => { + document.body.innerHTML = ` + <button class="btn">Normal</button> + <button class="btn disabled">Disabled</button> + <button class="btn">Another</button> + ` + + // Select buttons that are NOT disabled + const activeButtons = document.querySelectorAll('.btn:not(.disabled)') + + expect(activeButtons.length).toBe(2) + expect(activeButtons[0].textContent).toBe('Normal') + expect(activeButtons[1].textContent).toBe('Another') + }) + + it('should select elements using :first-of-type pseudo-selector', () => { + document.body.innerHTML = ` + <div class="container"> + <span>First span</span> + <p>First paragraph</p> + <span>Second span</span> + <p>Second paragraph</p> + </div> + ` + + const firstSpan = document.querySelector('.container span:first-of-type') + const firstP = document.querySelector('.container p:first-of-type') + + expect(firstSpan.textContent).toBe('First span') + expect(firstP.textContent).toBe('First paragraph') + }) + }) + + describe('Common Misconceptions', () => { + describe('Misconception 1: DOM vs HTML source', () => { + it('should show browser always has head and body in DOM', () => { + // Browser fixes missing structure + expect(document.head).toBeDefined() + expect(document.body).toBeDefined() + expect(document.documentElement).toBeDefined() + }) + + it('should show DOM reflects JavaScript changes', () => { + document.body.innerHTML = '<div id="test">Original</div>' + const div = document.getElementById('test') + div.textContent = 'Modified' + + // DOM reflects change + expect(div.textContent).toBe('Modified') + // Original HTML source would still say "Original" + }) + }) + + describe('Misconception 2: querySelector performance', () => { + it('should show both methods return the same element', () => { + document.body.innerHTML = '<div id="myId">Test</div>' + + const byId = document.getElementById('myId') + const byQuery = document.querySelector('#myId') + + expect(byId).toBe(byQuery) + }) + }) + + describe('Misconception 3: display none vs remove', () => { + it('should show display:none keeps element in DOM', () => { + document.body.innerHTML = '<div id="hidden">Content</div>' + const el = document.getElementById('hidden') + el.style.display = 'none' + + // Still in DOM! + expect(document.getElementById('hidden')).toBe(el) + expect(el.parentNode).toBe(document.body) + }) + + it('should show visibility:hidden keeps element in DOM', () => { + document.body.innerHTML = '<div id="invisible">Content</div>' + const el = document.getElementById('invisible') + el.style.visibility = 'hidden' + + expect(document.getElementById('invisible')).toBe(el) + }) + + it('should show remove() actually removes from DOM', () => { + document.body.innerHTML = '<div id="toRemove">Content</div>' + const el = document.getElementById('toRemove') + el.remove() + + expect(document.getElementById('toRemove')).toBeNull() + }) + }) + + describe('Misconception 4: Live collections gotcha', () => { + it('should show live collection changes when DOM changes', () => { + document.body.innerHTML = ` + <div class="item">1</div> + <div class="item">2</div> + <div class="item">3</div> + ` + + const liveCollection = document.getElementsByClassName('item') + expect(liveCollection.length).toBe(3) + + // Remove first item - collection shrinks + liveCollection[0].remove() + expect(liveCollection.length).toBe(2) + }) + + it('should show static NodeList is safe for iteration', () => { + document.body.innerHTML = ` + <div class="item">1</div> + <div class="item">2</div> + <div class="item">3</div> + ` + + const staticList = document.querySelectorAll('.item') + expect(staticList.length).toBe(3) + + // Remove all items safely + staticList.forEach(item => item.remove()) + + // Original NodeList still has references (but elements are removed from DOM) + expect(staticList.length).toBe(3) // Static snapshot + expect(document.querySelectorAll('.item').length).toBe(0) // DOM is empty + }) + }) + }) + + describe('Interview Questions', () => { + describe('Q1: querySelector vs getElementById', () => { + it('should demonstrate querySelector flexibility with complex selectors', () => { + document.body.innerHTML = ` + <div class="card"> + <span data-id="123">Content</span> + </div> + ` + + // querySelector can do what getElementById cannot + const byAttribute = document.querySelector('[data-id="123"]') + const firstCard = document.querySelector('.card:first-child') + + expect(byAttribute.textContent).toBe('Content') + expect(firstCard.className).toBe('card') + }) + + it('should show both return null for non-existent elements', () => { + document.body.innerHTML = '' + + expect(document.getElementById('nonexistent')).toBeNull() + expect(document.querySelector('#nonexistent')).toBeNull() + }) + }) + + describe('Q2: Event delegation', () => { + it('should demonstrate event delegation with closest()', () => { + document.body.innerHTML = ` + <div class="container"> + <div class="item" data-id="1">Item 1</div> + <div class="item" data-id="2">Item 2</div> + </div> + ` + + let clickedId = null + + // Simulated event delegation logic + const item = document.querySelector('[data-id="2"]') + const closestItem = item.closest('.item') + + if (closestItem) { + clickedId = closestItem.dataset.id + } + + expect(clickedId).toBe('2') + }) + + it('should show delegation works for dynamically added elements', () => { + document.body.innerHTML = '<ul class="list"></ul>' + const list = document.querySelector('.list') + + // Add item after "attaching" listener (simulated) + const newItem = document.createElement('li') + newItem.className = 'item' + newItem.textContent = 'New Item' + list.appendChild(newItem) + + // closest() finds it + expect(newItem.closest('.list')).toBe(list) + }) + }) + }) + }) +}) diff --git a/tests/web-platform/http-fetch/http-fetch.test.js b/tests/web-platform/http-fetch/http-fetch.test.js new file mode 100644 index 00000000..b95cdaed --- /dev/null +++ b/tests/web-platform/http-fetch/http-fetch.test.js @@ -0,0 +1,1224 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' + +// ============================================================================= +// HTTP & FETCH - TEST SUITE +// Tests for code examples from docs/concepts/http-fetch.mdx +// Uses Node 20+ native fetch with Vitest mocking +// ============================================================================= + +describe('HTTP & Fetch', () => { + // Store original fetch + const originalFetch = global.fetch + + beforeEach(() => { + // Reset fetch mock before each test + vi.restoreAllMocks() + }) + + afterEach(() => { + // Restore original fetch after each test + global.fetch = originalFetch + }) + + // =========================================================================== + // RESPONSE OBJECT BASICS + // =========================================================================== + describe('Response Object', () => { + it('should have status property', () => { + const response = new Response('OK', { status: 200 }) + expect(response.status).toBe(200) + }) + + it('should have statusText property', () => { + const response = new Response('OK', { status: 200, statusText: 'OK' }) + expect(response.statusText).toBe('OK') + }) + + it('should have ok property true for 2xx status', () => { + const response200 = new Response('OK', { status: 200 }) + const response201 = new Response('Created', { status: 201 }) + const response204 = new Response(null, { status: 204 }) + + expect(response200.ok).toBe(true) + expect(response201.ok).toBe(true) + expect(response204.ok).toBe(true) + }) + + it('should have ok property false for non-2xx status', () => { + const response400 = new Response('Bad Request', { status: 400 }) + const response404 = new Response('Not Found', { status: 404 }) + const response500 = new Response('Server Error', { status: 500 }) + + expect(response400.ok).toBe(false) + expect(response404.ok).toBe(false) + expect(response500.ok).toBe(false) + }) + + it('should have headers object', () => { + const response = new Response('OK', { + headers: { + 'Content-Type': 'application/json', + 'X-Custom-Header': 'custom-value' + } + }) + + expect(response.headers.get('Content-Type')).toBe('application/json') + expect(response.headers.get('X-Custom-Header')).toBe('custom-value') + }) + + it('should parse JSON body', async () => { + const data = { name: 'Alice', age: 30 } + const response = new Response(JSON.stringify(data), { + headers: { 'Content-Type': 'application/json' } + }) + + const parsed = await response.json() + expect(parsed).toEqual(data) + }) + + it('should parse text body', async () => { + const response = new Response('Hello, World!') + const text = await response.text() + expect(text).toBe('Hello, World!') + }) + + it('should only allow body to be read once', async () => { + const response = new Response('Hello') + + await response.text() // First read + + // Second read should throw + await expect(response.text()).rejects.toThrow() + }) + + it('should clone response for multiple reads', async () => { + const response = new Response('Hello') + const clone = response.clone() + + const text1 = await response.text() + const text2 = await clone.text() + + expect(text1).toBe('Hello') + expect(text2).toBe('Hello') + }) + }) + + // =========================================================================== + // RESPONSE BODY METHODS (blob, arrayBuffer) + // =========================================================================== + describe('Response Body Methods', () => { + it('should parse body as Blob', async () => { + const textContent = 'Hello, World!' + const response = new Response(textContent) + const blob = await response.blob() + + expect(blob).toBeInstanceOf(Blob) + expect(blob.size).toBe(textContent.length) + }) + + it('should parse body as ArrayBuffer', async () => { + const textContent = 'Hello' + const response = new Response(textContent) + const buffer = await response.arrayBuffer() + + expect(buffer).toBeInstanceOf(ArrayBuffer) + expect(buffer.byteLength).toBe(textContent.length) + }) + + it('should parse binary data as Blob', async () => { + const binaryData = new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f]) // "Hello" + const response = new Response(binaryData) + const blob = await response.blob() + + expect(blob.size).toBe(5) + }) + + it('should parse binary data as ArrayBuffer', async () => { + const binaryData = new Uint8Array([1, 2, 3, 4, 5]) + const response = new Response(binaryData) + const buffer = await response.arrayBuffer() + + const view = new Uint8Array(buffer) + expect(view[0]).toBe(1) + expect(view[4]).toBe(5) + }) + }) + + // =========================================================================== + // RESPONSE METADATA PROPERTIES + // =========================================================================== + describe('Response Metadata', () => { + it('should have url property', () => { + // Note: In real fetch, url reflects the final URL after redirects + // For Response constructor, we can't set URL directly + const response = new Response('OK', { status: 200 }) + expect(response.url).toBe('') // Empty for constructed responses + }) + + it('should have type property', () => { + const response = new Response('OK', { status: 200 }) + // Constructed responses have type "default" + expect(response.type).toBe('default') + }) + + it('should have redirected property', () => { + const response = new Response('OK', { status: 200 }) + // Constructed responses are not redirected + expect(response.redirected).toBe(false) + }) + + it('should have bodyUsed property', async () => { + const response = new Response('Hello') + + expect(response.bodyUsed).toBe(false) + await response.text() + expect(response.bodyUsed).toBe(true) + }) + }) + + // =========================================================================== + // STATUS CODE RANGES + // =========================================================================== + describe('HTTP Status Codes', () => { + describe('2xx Success', () => { + it('200 OK should be successful', () => { + const response = new Response('OK', { status: 200 }) + expect(response.ok).toBe(true) + expect(response.status).toBe(200) + }) + + it('201 Created should be successful', () => { + const response = new Response('Created', { status: 201 }) + expect(response.ok).toBe(true) + expect(response.status).toBe(201) + }) + + it('204 No Content should be successful', () => { + const response = new Response(null, { status: 204 }) + expect(response.ok).toBe(true) + expect(response.status).toBe(204) + }) + + it('299 should still be ok', () => { + const response = new Response('OK', { status: 299 }) + expect(response.ok).toBe(true) + }) + }) + + describe('3xx Redirection', () => { + it('301 Moved Permanently should not be ok', () => { + const response = new Response('Moved', { status: 301 }) + expect(response.ok).toBe(false) + }) + + it('302 Found should not be ok', () => { + const response = new Response('Found', { status: 302 }) + expect(response.ok).toBe(false) + }) + + it('304 Not Modified should not be ok', () => { + // 304 is a "null body status" so we use null body + const response = new Response(null, { status: 304 }) + expect(response.ok).toBe(false) + }) + }) + + describe('4xx Client Errors', () => { + it('400 Bad Request should not be ok', () => { + const response = new Response('Bad Request', { status: 400 }) + expect(response.ok).toBe(false) + expect(response.status).toBe(400) + }) + + it('401 Unauthorized should not be ok', () => { + const response = new Response('Unauthorized', { status: 401 }) + expect(response.ok).toBe(false) + expect(response.status).toBe(401) + }) + + it('403 Forbidden should not be ok', () => { + const response = new Response('Forbidden', { status: 403 }) + expect(response.ok).toBe(false) + expect(response.status).toBe(403) + }) + + it('404 Not Found should not be ok', () => { + const response = new Response('Not Found', { status: 404 }) + expect(response.ok).toBe(false) + expect(response.status).toBe(404) + }) + + it('422 Unprocessable Entity should not be ok', () => { + const response = new Response('Unprocessable Entity', { status: 422 }) + expect(response.ok).toBe(false) + expect(response.status).toBe(422) + }) + }) + + describe('5xx Server Errors', () => { + it('500 Internal Server Error should not be ok', () => { + const response = new Response('Internal Server Error', { status: 500 }) + expect(response.ok).toBe(false) + expect(response.status).toBe(500) + }) + + it('502 Bad Gateway should not be ok', () => { + const response = new Response('Bad Gateway', { status: 502 }) + expect(response.ok).toBe(false) + expect(response.status).toBe(502) + }) + + it('503 Service Unavailable should not be ok', () => { + const response = new Response('Service Unavailable', { status: 503 }) + expect(response.ok).toBe(false) + expect(response.status).toBe(503) + }) + }) + }) + + // =========================================================================== + // HEADERS API + // =========================================================================== + describe('Headers API', () => { + it('should create headers from object', () => { + const headers = new Headers({ + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }) + + expect(headers.get('Content-Type')).toBe('application/json') + expect(headers.get('Accept')).toBe('application/json') + }) + + it('should append headers', () => { + const headers = new Headers() + headers.append('Accept', 'application/json') + headers.append('Accept', 'text/plain') + + expect(headers.get('Accept')).toBe('application/json, text/plain') + }) + + it('should set headers (overwrite)', () => { + const headers = new Headers() + headers.set('Accept', 'application/json') + headers.set('Accept', 'text/plain') + + expect(headers.get('Accept')).toBe('text/plain') + }) + + it('should check if header exists', () => { + const headers = new Headers({ + 'Content-Type': 'application/json' + }) + + expect(headers.has('Content-Type')).toBe(true) + expect(headers.has('Authorization')).toBe(false) + }) + + it('should delete headers', () => { + const headers = new Headers({ + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }) + + headers.delete('Accept') + + expect(headers.has('Accept')).toBe(false) + expect(headers.has('Content-Type')).toBe(true) + }) + + it('should iterate over headers', () => { + const headers = new Headers({ + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }) + + const entries = [] + for (const [key, value] of headers) { + entries.push([key, value]) + } + + expect(entries).toContainEqual(['content-type', 'application/json']) + expect(entries).toContainEqual(['accept', 'application/json']) + }) + + it('should be case-insensitive for header names', () => { + const headers = new Headers({ + 'Content-Type': 'application/json' + }) + + expect(headers.get('content-type')).toBe('application/json') + expect(headers.get('CONTENT-TYPE')).toBe('application/json') + expect(headers.get('Content-Type')).toBe('application/json') + }) + }) + + // =========================================================================== + // REQUEST OBJECT + // =========================================================================== + describe('Request Object', () => { + it('should create a basic request', () => { + const request = new Request('https://api.example.com/users') + + expect(request.url).toBe('https://api.example.com/users') + expect(request.method).toBe('GET') + }) + + it('should create request with method', () => { + const request = new Request('https://api.example.com/users', { + method: 'POST' + }) + + expect(request.method).toBe('POST') + }) + + it('should create request with headers', () => { + const request = new Request('https://api.example.com/users', { + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer token123' + } + }) + + expect(request.headers.get('Content-Type')).toBe('application/json') + expect(request.headers.get('Authorization')).toBe('Bearer token123') + }) + + it('should create request with body', async () => { + const body = JSON.stringify({ name: 'Alice' }) + const request = new Request('https://api.example.com/users', { + method: 'POST', + body: body + }) + + const requestBody = await request.text() + expect(requestBody).toBe(body) + }) + + it('should clone request', () => { + const request = new Request('https://api.example.com/users', { + method: 'POST', + headers: { 'Content-Type': 'application/json' } + }) + + const clone = request.clone() + + expect(clone.url).toBe(request.url) + expect(clone.method).toBe(request.method) + expect(clone.headers.get('Content-Type')).toBe('application/json') + }) + }) + + // =========================================================================== + // FETCH WITH MOCKING + // =========================================================================== + describe('Fetch API (Mocked)', () => { + it('should make a GET request', async () => { + const mockData = { id: 1, name: 'Alice' } + global.fetch = vi.fn().mockResolvedValue( + new Response(JSON.stringify(mockData), { + status: 200, + headers: { 'Content-Type': 'application/json' } + }) + ) + + const response = await fetch('https://api.example.com/users/1') + const data = await response.json() + + expect(fetch).toHaveBeenCalledWith('https://api.example.com/users/1') + expect(data).toEqual(mockData) + }) + + it('should make a POST request with body', async () => { + const mockResponse = { id: 1, name: 'Alice' } + global.fetch = vi.fn().mockResolvedValue( + new Response(JSON.stringify(mockResponse), { + status: 201, + headers: { 'Content-Type': 'application/json' } + }) + ) + + const userData = { name: 'Alice', email: 'alice@example.com' } + const response = await fetch('https://api.example.com/users', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(userData) + }) + + expect(fetch).toHaveBeenCalledWith('https://api.example.com/users', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(userData) + }) + expect(response.status).toBe(201) + }) + + it('should handle 404 response (not rejection)', async () => { + global.fetch = vi.fn().mockResolvedValue( + new Response('Not Found', { status: 404 }) + ) + + // Fetch resolves even for 404! + const response = await fetch('https://api.example.com/users/999') + + expect(response.ok).toBe(false) + expect(response.status).toBe(404) + }) + + it('should handle 500 response (not rejection)', async () => { + global.fetch = vi.fn().mockResolvedValue( + new Response('Internal Server Error', { status: 500 }) + ) + + // Fetch resolves even for 500! + const response = await fetch('https://api.example.com/broken') + + expect(response.ok).toBe(false) + expect(response.status).toBe(500) + }) + + it('should reject on network error', async () => { + global.fetch = vi.fn().mockRejectedValue( + new TypeError('Failed to fetch') + ) + + await expect(fetch('https://api.example.com/unreachable')) + .rejects.toThrow('Failed to fetch') + }) + }) + + // =========================================================================== + // ERROR HANDLING PATTERNS + // =========================================================================== + describe('Error Handling Patterns', () => { + it('should properly check response.ok', async () => { + global.fetch = vi.fn().mockResolvedValue( + new Response('Not Found', { status: 404 }) + ) + + const response = await fetch('/api/users/999') + + // The correct pattern + if (!response.ok) { + const error = new Error(`HTTP ${response.status}`) + expect(error.message).toBe('HTTP 404') + } + }) + + it('should demonstrate fetchJSON wrapper pattern', async () => { + // Reusable fetch wrapper + async function fetchJSON(url, options = {}) { + const response = await fetch(url, options) + + if (!response.ok) { + const error = new Error(`HTTP ${response.status}`) + error.status = response.status + throw error + } + + if (response.status === 204) { + return null + } + + return response.json() + } + + // Test successful response + global.fetch = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ id: 1 }), { status: 200 }) + ) + + const data = await fetchJSON('/api/users/1') + expect(data).toEqual({ id: 1 }) + + // Test 404 error + global.fetch = vi.fn().mockResolvedValue( + new Response('Not Found', { status: 404 }) + ) + + try { + await fetchJSON('/api/users/999') + expect.fail('Should have thrown') + } catch (error) { + expect(error.message).toBe('HTTP 404') + expect(error.status).toBe(404) + } + + // Test 204 No Content + global.fetch = vi.fn().mockResolvedValue( + new Response(null, { status: 204 }) + ) + + const noContent = await fetchJSON('/api/users/1', { method: 'DELETE' }) + expect(noContent).toBeNull() + }) + + it('should differentiate network vs HTTP errors', async () => { + let errorType = null + + async function safeFetch(url) { + try { + const response = await fetch(url) + + if (!response.ok) { + errorType = 'http' + throw new Error(`HTTP ${response.status}`) + } + + return await response.json() + } catch (error) { + if (error.message.startsWith('HTTP')) { + // Already handled HTTP error + throw error + } + // Network error + errorType = 'network' + throw new Error('Network error: ' + error.message) + } + } + + // Test HTTP error (404) + global.fetch = vi.fn().mockResolvedValue( + new Response('Not Found', { status: 404 }) + ) + + try { + await safeFetch('/api/missing') + } catch (e) { + expect(errorType).toBe('http') + } + + // Test network error + global.fetch = vi.fn().mockRejectedValue( + new TypeError('Failed to fetch') + ) + + try { + await safeFetch('/api/unreachable') + } catch (e) { + expect(errorType).toBe('network') + } + }) + }) + + // =========================================================================== + // ABORT CONTROLLER + // =========================================================================== + describe('AbortController', () => { + it('should create an AbortController', () => { + const controller = new AbortController() + + expect(controller).toBeDefined() + expect(controller.signal).toBeDefined() + expect(controller.signal.aborted).toBe(false) + }) + + it('should abort and update signal', () => { + const controller = new AbortController() + + expect(controller.signal.aborted).toBe(false) + controller.abort() + expect(controller.signal.aborted).toBe(true) + }) + + it('should abort with reason', () => { + const controller = new AbortController() + controller.abort('User cancelled') + + expect(controller.signal.aborted).toBe(true) + expect(controller.signal.reason).toBe('User cancelled') + }) + + it('should reject fetch when aborted', async () => { + const controller = new AbortController() + + // Mock fetch to respect abort signal + global.fetch = vi.fn().mockImplementation((url, options) => { + return new Promise((resolve, reject) => { + if (options?.signal?.aborted) { + const error = new DOMException('The operation was aborted.', 'AbortError') + reject(error) + return + } + + options?.signal?.addEventListener('abort', () => { + const error = new DOMException('The operation was aborted.', 'AbortError') + reject(error) + }) + + // Simulate slow request + setTimeout(() => { + resolve(new Response('OK')) + }, 1000) + }) + }) + + const fetchPromise = fetch('/api/slow', { signal: controller.signal }) + + // Abort immediately + controller.abort() + + await expect(fetchPromise).rejects.toThrow() + + try { + await fetchPromise + } catch (error) { + expect(error.name).toBe('AbortError') + } + }) + + it('should handle abort in try/catch', async () => { + const controller = new AbortController() + controller.abort() + + global.fetch = vi.fn().mockRejectedValue( + new DOMException('The operation was aborted.', 'AbortError') + ) + + try { + await fetch('/api/data', { signal: controller.signal }) + expect.fail('Should have thrown') + } catch (error) { + if (error.name === 'AbortError') { + // Expected - request was cancelled + expect(true).toBe(true) + } else { + throw error + } + } + }) + + it('should implement timeout pattern', async () => { + async function fetchWithTimeout(url, timeout = 5000) { + const controller = new AbortController() + const timeoutId = setTimeout(() => controller.abort(), timeout) + + try { + const response = await fetch(url, { signal: controller.signal }) + clearTimeout(timeoutId) + return response + } catch (error) { + clearTimeout(timeoutId) + if (error.name === 'AbortError') { + throw new Error(`Request timed out after ${timeout}ms`) + } + throw error + } + } + + // Mock slow request that will be aborted + global.fetch = vi.fn().mockImplementation((url, options) => { + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + resolve(new Response('OK')) + }, 10000) // Very slow + + options?.signal?.addEventListener('abort', () => { + clearTimeout(timeout) + reject(new DOMException('The operation was aborted.', 'AbortError')) + }) + }) + }) + + // Should timeout after 100ms + await expect(fetchWithTimeout('/api/slow', 100)) + .rejects.toThrow('Request timed out after 100ms') + }) + }) + + // =========================================================================== + // PARALLEL REQUESTS + // =========================================================================== + describe('Parallel Requests with Promise.all', () => { + it('should fetch multiple resources in parallel', async () => { + global.fetch = vi.fn() + .mockResolvedValueOnce(new Response(JSON.stringify({ id: 1, name: 'User' }))) + .mockResolvedValueOnce(new Response(JSON.stringify([{ id: 1, title: 'Post' }]))) + .mockResolvedValueOnce(new Response(JSON.stringify([{ id: 1, body: 'Comment' }]))) + + const [user, posts, comments] = await Promise.all([ + fetch('/api/user').then(r => r.json()), + fetch('/api/posts').then(r => r.json()), + fetch('/api/comments').then(r => r.json()) + ]) + + expect(user).toEqual({ id: 1, name: 'User' }) + expect(posts).toEqual([{ id: 1, title: 'Post' }]) + expect(comments).toEqual([{ id: 1, body: 'Comment' }]) + expect(fetch).toHaveBeenCalledTimes(3) + }) + + it('should fail fast if any request fails', async () => { + global.fetch = vi.fn() + .mockResolvedValueOnce(new Response(JSON.stringify({ id: 1 }))) + .mockRejectedValueOnce(new Error('Network error')) + .mockResolvedValueOnce(new Response(JSON.stringify({ id: 3 }))) + + await expect(Promise.all([ + fetch('/api/1').then(r => r.json()), + fetch('/api/2').then(r => r.json()), + fetch('/api/3').then(r => r.json()) + ])).rejects.toThrow('Network error') + }) + + it('should use Promise.allSettled for graceful handling', async () => { + global.fetch = vi.fn() + .mockResolvedValueOnce(new Response(JSON.stringify({ id: 1 }))) + .mockRejectedValueOnce(new Error('Failed')) + .mockResolvedValueOnce(new Response(JSON.stringify({ id: 3 }))) + + const results = await Promise.allSettled([ + fetch('/api/1').then(r => r.json()), + fetch('/api/2').then(r => r.json()), + fetch('/api/3').then(r => r.json()) + ]) + + expect(results[0].status).toBe('fulfilled') + expect(results[0].value).toEqual({ id: 1 }) + + expect(results[1].status).toBe('rejected') + expect(results[1].reason.message).toBe('Failed') + + expect(results[2].status).toBe('fulfilled') + expect(results[2].value).toEqual({ id: 3 }) + }) + }) + + // =========================================================================== + // LOADING STATE PATTERN + // =========================================================================== + describe('Loading State Pattern', () => { + it('should track loading, data, and error states', async () => { + async function fetchWithState(url) { + const state = { + data: null, + loading: true, + error: null + } + + try { + const response = await fetch(url) + + if (!response.ok) { + throw new Error(`HTTP ${response.status}`) + } + + state.data = await response.json() + } catch (error) { + state.error = error.message + } finally { + state.loading = false + } + + return state + } + + // Test successful fetch + global.fetch = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ name: 'Alice' }), { status: 200 }) + ) + + const successState = await fetchWithState('/api/user') + expect(successState.loading).toBe(false) + expect(successState.data).toEqual({ name: 'Alice' }) + expect(successState.error).toBeNull() + + // Test error fetch + global.fetch = vi.fn().mockResolvedValue( + new Response('Not Found', { status: 404 }) + ) + + const errorState = await fetchWithState('/api/missing') + expect(errorState.loading).toBe(false) + expect(errorState.data).toBeNull() + expect(errorState.error).toBe('HTTP 404') + + // Test network error + global.fetch = vi.fn().mockRejectedValue(new Error('Network error')) + + const networkErrorState = await fetchWithState('/api/unreachable') + expect(networkErrorState.loading).toBe(false) + expect(networkErrorState.data).toBeNull() + expect(networkErrorState.error).toBe('Network error') + }) + }) + + // =========================================================================== + // JSON PARSING + // =========================================================================== + describe('JSON Parsing', () => { + it('should parse valid JSON', async () => { + const response = new Response('{"name": "Alice", "age": 30}') + const data = await response.json() + + expect(data).toEqual({ name: 'Alice', age: 30 }) + }) + + it('should parse JSON arrays', async () => { + const response = new Response('[1, 2, 3, 4, 5]') + const data = await response.json() + + expect(data).toEqual([1, 2, 3, 4, 5]) + }) + + it('should parse nested JSON', async () => { + const nested = { + user: { + name: 'Alice', + address: { + city: 'Wonderland' + } + }, + posts: [ + { id: 1, title: 'First' }, + { id: 2, title: 'Second' } + ] + } + + const response = new Response(JSON.stringify(nested)) + const data = await response.json() + + expect(data.user.address.city).toBe('Wonderland') + expect(data.posts[1].title).toBe('Second') + }) + + it('should throw on invalid JSON', async () => { + const response = new Response('not valid json {') + + await expect(response.json()).rejects.toThrow() + }) + + it('should throw on empty body when expecting JSON', async () => { + const response = new Response('') + + await expect(response.json()).rejects.toThrow() + }) + }) + + // =========================================================================== + // HTTP METHODS + // =========================================================================== + describe('HTTP Methods', () => { + beforeEach(() => { + global.fetch = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ success: true }), { status: 200 }) + ) + }) + + it('should default to GET method', async () => { + await fetch('/api/users') + + expect(fetch).toHaveBeenCalledWith('/api/users') + }) + + it('should make POST request', async () => { + await fetch('/api/users', { + method: 'POST', + body: JSON.stringify({ name: 'Alice' }) + }) + + expect(fetch).toHaveBeenCalledWith('/api/users', expect.objectContaining({ + method: 'POST' + })) + }) + + it('should make PUT request', async () => { + await fetch('/api/users/1', { + method: 'PUT', + body: JSON.stringify({ name: 'Alice Updated' }) + }) + + expect(fetch).toHaveBeenCalledWith('/api/users/1', expect.objectContaining({ + method: 'PUT' + })) + }) + + it('should make PATCH request', async () => { + await fetch('/api/users/1', { + method: 'PATCH', + body: JSON.stringify({ name: 'New Name' }) + }) + + expect(fetch).toHaveBeenCalledWith('/api/users/1', expect.objectContaining({ + method: 'PATCH' + })) + }) + + it('should make DELETE request', async () => { + await fetch('/api/users/1', { + method: 'DELETE' + }) + + expect(fetch).toHaveBeenCalledWith('/api/users/1', expect.objectContaining({ + method: 'DELETE' + })) + }) + }) + + // =========================================================================== + // URL AND QUERY PARAMETERS + // =========================================================================== + describe('URL and Query Parameters', () => { + it('should construct URL with search params', () => { + const url = new URL('https://api.example.com/search') + url.searchParams.set('q', 'javascript') + url.searchParams.set('page', '1') + url.searchParams.set('limit', '10') + + expect(url.toString()).toBe('https://api.example.com/search?q=javascript&page=1&limit=10') + }) + + it('should append multiple values for same param', () => { + const url = new URL('https://api.example.com/filter') + url.searchParams.append('tag', 'javascript') + url.searchParams.append('tag', 'nodejs') + + expect(url.toString()).toBe('https://api.example.com/filter?tag=javascript&tag=nodejs') + }) + + it('should get search params', () => { + const url = new URL('https://api.example.com/search?q=javascript&page=2') + + expect(url.searchParams.get('q')).toBe('javascript') + expect(url.searchParams.get('page')).toBe('2') + expect(url.searchParams.get('missing')).toBeNull() + }) + + it('should delete search params', () => { + const url = new URL('https://api.example.com/search?q=javascript&page=2') + url.searchParams.delete('page') + + expect(url.toString()).toBe('https://api.example.com/search?q=javascript') + }) + + it('should check if param exists', () => { + const url = new URL('https://api.example.com/search?q=javascript') + + expect(url.searchParams.has('q')).toBe(true) + expect(url.searchParams.has('page')).toBe(false) + }) + + it('should use URLSearchParams with fetch', async () => { + global.fetch = vi.fn().mockResolvedValue( + new Response(JSON.stringify([]), { status: 200 }) + ) + + const params = new URLSearchParams({ + q: 'javascript', + page: '1' + }) + + await fetch(`/api/search?${params}`) + + expect(fetch).toHaveBeenCalledWith('/api/search?q=javascript&page=1') + }) + }) + + // =========================================================================== + // REAL WORLD PATTERNS + // =========================================================================== + describe('Real World Patterns', () => { + it('should implement retry logic', async () => { + let attempts = 0 + + async function fetchWithRetry(url, options = {}, retries = 3) { + for (let i = 0; i < retries; i++) { + try { + attempts++ + const response = await fetch(url, options) + if (response.ok) return response + if (response.status >= 500 && i < retries - 1) continue + throw new Error(`HTTP ${response.status}`) + } catch (error) { + if (i === retries - 1) throw error + } + } + } + + // Mock: fail twice, succeed on third + global.fetch = vi.fn() + .mockResolvedValueOnce(new Response('Error', { status: 500 })) + .mockResolvedValueOnce(new Response('Error', { status: 500 })) + .mockResolvedValueOnce(new Response('OK', { status: 200 })) + + const response = await fetchWithRetry('/api/flaky') + + expect(response.status).toBe(200) + expect(attempts).toBe(3) + }) + + it('should implement search with cancel previous', async () => { + let currentController = null + + async function searchWithCancel(query) { + // Cancel previous request + if (currentController) { + currentController.abort() + } + + currentController = new AbortController() + + const response = await fetch(`/api/search?q=${query}`, { + signal: currentController.signal + }) + + return response.json() + } + + // Mock fetch that respects abort + global.fetch = vi.fn().mockImplementation((url, options) => { + return new Promise((resolve, reject) => { + if (options?.signal?.aborted) { + reject(new DOMException('Aborted', 'AbortError')) + return + } + + const handler = () => { + reject(new DOMException('Aborted', 'AbortError')) + } + + options?.signal?.addEventListener('abort', handler) + + // Resolve after short delay + setTimeout(() => { + options?.signal?.removeEventListener('abort', handler) + resolve(new Response(JSON.stringify({ results: [url] }))) + }, 50) + }) + }) + + // Start first search + const search1 = searchWithCancel('java') + + // Start second search (should cancel first) + const search2 = searchWithCancel('javascript') + + // First should be aborted + await expect(search1).rejects.toThrow() + + // Second should succeed + const result = await search2 + expect(result.results[0]).toContain('javascript') + }) + }) + + // =========================================================================== + // FORMDATA + // =========================================================================== + describe('FormData', () => { + it('should create FormData object', () => { + const formData = new FormData() + formData.append('username', 'alice') + formData.append('email', 'alice@example.com') + + expect(formData.get('username')).toBe('alice') + expect(formData.get('email')).toBe('alice@example.com') + }) + + it('should append multiple values for same key', () => { + const formData = new FormData() + formData.append('tags', 'javascript') + formData.append('tags', 'nodejs') + + const tags = formData.getAll('tags') + expect(tags).toEqual(['javascript', 'nodejs']) + }) + + it('should set value (overwrite)', () => { + const formData = new FormData() + formData.append('name', 'alice') + formData.set('name', 'bob') + + expect(formData.get('name')).toBe('bob') + }) + + it('should check if key exists', () => { + const formData = new FormData() + formData.append('username', 'alice') + + expect(formData.has('username')).toBe(true) + expect(formData.has('password')).toBe(false) + }) + + it('should delete key', () => { + const formData = new FormData() + formData.append('username', 'alice') + formData.append('email', 'alice@example.com') + + formData.delete('email') + + expect(formData.has('email')).toBe(false) + expect(formData.has('username')).toBe(true) + }) + + it('should iterate over entries', () => { + const formData = new FormData() + formData.append('name', 'alice') + formData.append('age', '30') + + const entries = [] + for (const [key, value] of formData) { + entries.push([key, value]) + } + + expect(entries).toContainEqual(['name', 'alice']) + expect(entries).toContainEqual(['age', '30']) + }) + + it('should send FormData with fetch (no Content-Type header needed)', async () => { + global.fetch = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ success: true }), { status: 200 }) + ) + + const formData = new FormData() + formData.append('username', 'alice') + formData.append('avatar', new Blob(['fake image data'], { type: 'image/png' }), 'avatar.png') + + await fetch('/api/profile', { + method: 'POST', + body: formData + // Note: Don't set Content-Type header - browser sets it automatically with boundary + }) + + expect(fetch).toHaveBeenCalledWith('/api/profile', expect.objectContaining({ + method: 'POST', + body: expect.any(FormData) + })) + }) + + it('should parse FormData from response', async () => { + // Create a FormData-like body + const formData = new FormData() + formData.append('field1', 'value1') + formData.append('field2', 'value2') + + // Note: In real browsers, response.formData() parses multipart responses + // For testing, we verify the FormData API works correctly + expect(formData.get('field1')).toBe('value1') + expect(formData.get('field2')).toBe('value2') + }) + + it('should append File objects', () => { + const formData = new FormData() + const file = new File(['hello world'], 'test.txt', { type: 'text/plain' }) + + formData.append('document', file) + + const retrieved = formData.get('document') + expect(retrieved).toBeInstanceOf(File) + expect(retrieved.name).toBe('test.txt') + expect(retrieved.type).toBe('text/plain') + }) + + it('should append Blob objects with filename', () => { + const formData = new FormData() + const blob = new Blob(['image data'], { type: 'image/jpeg' }) + + formData.append('image', blob, 'photo.jpg') + + const retrieved = formData.get('image') + expect(retrieved).toBeInstanceOf(File) // Blob with filename becomes File + expect(retrieved.name).toBe('photo.jpg') + }) + }) +}) diff --git a/vitest.config.js b/vitest.config.js new file mode 100644 index 00000000..3bf505d5 --- /dev/null +++ b/vitest.config.js @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + include: ['tests/**/*.test.js'], + globals: false, + environment: 'node' + } +})