JavaScript
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Types
-
Section 3: Strings and Numbers in Depth
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Objects and Arrays
-
Section 7: Maps, Sets, and Symbols
-
Section 8: Asynchronous JavaScript
-
Section 9: Object-Oriented and Prototypes
-
Section 10: The DOM
-
Section 11: Browser APIs
-
Section 12: Modern JavaScript (ES2015-ES2025)
-
Section 13: Functional Programming Patterns
-
Section 14: Error Handling and Debugging
-
Section 15: Testing
-
Section 16: Accessibility for JavaScript Developers
-
Section 17: Internationalization and Localization
-
Section 18: Performance
-
Section 19: Node.js Fundamentals
-
Section 20: Regular Expressions
-
Section 21: Design Patterns in JavaScript
-
Section 22: Security Basics
-
Section 23: Data Structures and Algorithms in JavaScript
-
Section 24: Practical Projects
-
Section 25: More Advanced Async Patterns
-
Section 26: More Object and Class Practice
-
Section 27: Working with Dates and Internationalization
-
Section 28: Web Components
-
Section 29: More DOM and Browser Practice
-
Section 30: Build Tooling for Vanilla JavaScript
-
Section 31: More Practice Projects
-
Section 32: Interview and Algorithm Practice
-
Section 33: Error Objects (MDN Reference)
-
Section 34: TypedArrays and Binary Data
-
Section 35: Reflection and Metaprogramming (MDN Reference)
-
Section 36: More Global Functions (MDN Reference)
54: Symbols and Their Use Cases
I want to show you a problem I ran into recently while building a plugin system for a project. I had this user object coming from an API, and I needed to attach some internal metadata to it—basically some "private" tracking info—that shouldn't interfere with the actual user data. My first instinct was to just use a string key with an underscore, which is a common convention for "private" properties.
const user = {
name: 'Alex',
email: 'alex@example.com'
};
// Adding some internal tracking
user._internalId = 'track_9921';
console.log(user);
// { name: 'Alex', email: 'alex@example.com', _internalId: 'track_9921' }
The Collision Headache
This works fine until you realize that in a large codebase, you aren't the only one writing code. Imagine another developer—or a third-party library you installed—also decides to use _internalId to track something completely different. Suddenly, they overwrite your data, or you overwrite theirs, and you're spending four hours debugging a "ghost in the machine" because two different parts of the app are fighting over the same property name.
I wondered if there was a way to create a key that is guaranteed to be unique, regardless of what string name I give it. That's where Symbol comes in. Let's try replacing that string with a Symbol.
const internalId = Symbol('internalId');
user[internalId] = 'track_9921';
console.log(user[internalId]); // 'track_9921'
Now, you might look at Symbol('internalId') and think, "Wait, isn't that just a string in disguise?" I thought the same thing. So, I tried to create another Symbol with the exact same description to see if they'd match.
const id1 = Symbol('internalId');
const id2 = Symbol('internalId');
console.log(id1 === id2); // false
This is the "aha!" moment. Even though they have the same label, they are entirely distinct entities. If I use id1 as a key in my plugin and another dev uses id2 in theirs, we can both attach data to the same user object and never, ever collide. The label 'internalId' is just for us humans to make debugging easier; JS ignores it during comparisons.
Vanishing Acts and Object Keys
While I was playing with this, I noticed something interesting. Usually, when I'm logging objects or iterating through them to build a UI list, I use Object.keys() or a for...in loop. I decided to see if my new Symbol property would show up there.
const user = { name: 'Alex' };
const secretKey = Symbol('secret');
user[secretKey] = 'shhh!';
console.log(Object.keys(user)); // ['name']
console.log(JSON.stringify(user)); // {"name":"Alex"}
for (let key in user) {
console.log(key); // only logs 'name'
}
The Symbol is effectively invisible to standard iteration. It's not "private" in the sense of a hard security wall (don't mistake this for true encapsulation), but it is non-enumerable. This is incredibly useful. It means I can attach metadata to an object without breaking existing logic that expects the object to have a specific set of keys, and it won't accidentally get sent back to a server during a JSON.stringify() call.
Getting the Keys Back
Of course, if it's invisible, I can't debug it. I needed a way to see what Symbols were actually attached to the object. I dug into the documentation and found a specific method for this.
const symbols = Object.getOwnPropertySymbols(user);
console.log(symbols); // [ Symbol(secret) ]
console.log(user[symbols[0]]); // 'shhh!'
So, here is the mental model I use now: Use strings for data that defines the object and should be public. Use Symbols for "sidecar" data—things the object needs to function or be tracked, but which shouldn't interfere with the object's primary identity or be exposed during routine iterations.
📋 Practical Task
Exercise: Implementing a Collision-Proof Metadata Store for User Profiles
You are building a system where multiple different "Analytics Plugins" attach their own unique tracking IDs to a shared user object. If you use strings, the plugins will overwrite each other.
Your Task:
- Create a
userobject withusernameandemail. - Create two distinct Symbols: one called
pluginAIdand one calledpluginBId. - Attach a unique ID value to the
userobject using both Symbols. - Verify that:
Object.keys(user)does NOT contain the Symbols.- You can still retrieve both unique IDs from the
userobject using the Symbol variables. JSON.stringify(user)only shows theusernameandemail.
There are no comments for now.