ES6+ Features: Symbols
Relevant source files
The following files were used as context for generating this wiki page:Symbols are a primitive data type introduced in ECMAScript 6 (ES6), primarily used to create unique object property keys. The Jurassic engine provides a comprehensive and standards-compliant implementation of Symbols, enabling their use within the .NET environment. This includes the Symbol primitive, the Symbol global object, a global symbol registry, and most well-known symbols.
According to the project's documentation, the implementation of Symbol is complete, and support for well-known symbols is nearly complete. This allows developers to leverage modern JavaScript patterns that rely on symbols for metaprogramming, such as defining an object's iteration behavior or customizing how it's treated by certain language constructs.
Sources: README.md, Jurassic/Library/Symbol/Symbol.cs, Jurassic/Library/Symbol/SymbolConstructor.cs
Core Implementation
The implementation of Symbols in Jurassic is divided into two main classes: Symbol, which represents the primitive value, and SymbolConstructor, which represents the global Symbol function used to create them.
Sources: Jurassic/Library/Symbol/Symbol.cs, Jurassic/Library/Symbol/SymbolConstructor.cs
The Symbol Class
The Symbol class in Jurassic represents an individual, unique, and immutable symbol value. It is a sealed class that inherits from ObjectInstance, which is the base for all JavaScript objects in the engine.
Key characteristics of the Symbol class:
- Uniqueness: Each symbol instance is unique. This is guaranteed by an internal static
nextIdcounter that assigns a uniqueidto every new symbol. - Description: A symbol can have an optional string description, which is used for debugging and its
ToString()representation. - Immutability: Once created, a symbol's value and description cannot be changed.
- Reference Equality: Two symbols are only equal if they are the exact same instance.
The following diagram illustrates the structure of the Symbol and SymbolConstructor classes.
The SymbolConstructor Class
The SymbolConstructor is a static class that implements the global Symbol object. It is not a true constructor, as new Symbol() is disallowed and will throw a TypeError. Instead, symbols are created by calling Symbol() as a function.
Creating Symbols
There are two ways to create symbols:
Symbol(description): Creates a new, unique symbol every time it is called. The optionaldescriptionis for debugging.Symbol.for(key): Accesses a global symbol registry. If a symbol with the givenkeyalready exists in the registry, it is returned. Otherwise, a new symbol is created, added to the registry with that key, and then returned.
The Symbol.keyFor(symbol) method does the reverse of Symbol.for(), returning the key for a given symbol from the global registry, or undefined if the symbol is not in the registry.
SymbolConstructor Methods
| Method | Description |
|---|---|
Call(string description = null) | Creates and returns a new unique Symbol object. Marked with JSFunctionFlags.NonConstructible. |
For(string key) | Searches for a symbol with the given key in the global symbol registry and returns it. If not found, a new symbol is created and added to the registry. |
KeyFor(Symbol symbol) | Returns a shared-symbol key from the global symbol registry for the given symbol. |
Sources: Jurassic/Library/Symbol/SymbolConstructor.cs:28-150
Behavior and Usage
The unit tests and compatibility tests for Jurassic demonstrate the expected ES6 behavior for symbols.
Uniqueness and Type
A key feature of symbols is their uniqueness. Even symbols with the same description are not equal. The typeof operator correctly identifies them as a "symbol".
// From Unit Tests/Library/SymbolTests.cs
[TestMethod]
public void TypeOf()
{
Assert.AreEqual("symbol", Evaluate("typeof Symbol()"));
Assert.AreEqual("symbol", Evaluate("typeof Symbol('foo')"));
Assert.AreEqual("symbol", Evaluate("typeof Symbol.iterator"));
}
[TestMethod]
public void Equals()
{
// Two symbols are not equal.
Assert.AreEqual(false, Evaluate("Symbol() == Symbol()"));
Assert.AreEqual(false, Evaluate("Symbol('foo') == Symbol('foo')"));
}
Sources: Unit Tests/Library/SymbolTests.cs:20-36
Type Coercion
Symbols cannot be implicitly converted to a string or a number. Attempting to do so, for example by concatenating them with a string or adding them to a number, will result in a TypeError. Explicit conversion using String(symbol) is allowed.
// From Unit Tests/Library/SymbolTests.cs
[TestMethod]
public void ImplicitConversion()
{
Evaluate("var s = Symbol('foo')");
Assert.AreEqual("Symbol(foo)", Evaluate("String(s)"));
Assert.ExpectException<JavaScriptException>(() => Evaluate("s + ''"), "TypeError: Cannot convert a Symbol value to a string");
Assert.ExpectException<JavaScriptException>(() => Evaluate("s + 1"), "TypeError: Cannot convert a Symbol value to a number");
}
Sources: Unit Tests/Library/SymbolTests.cs:38-47, Test Suite Runner/Kangax/compat-table.json:11536-11580
Symbols as Property Keys
Symbols are primarily used as non-string property keys. This helps avoid name collisions. Symbol-keyed properties are not enumerable in for...in loops and are ignored by Object.keys() and JSON.stringify(). They can be accessed using Object.getOwnPropertySymbols().
The following diagram shows the sequence of using a symbol as a property key.
Well-Known Symbols
Well-known symbols are special built-in symbols, exposed as static properties of the Symbol object. They are used by the JavaScript engine to define the internal behavior of objects. For instance, Symbol.iterator specifies an object's default iterator, used by for...of loops.
Jurassic implements most of these, including:
| Symbol | Purpose |
|---|---|
Symbol.hasInstance | A method that determines if a constructor object recognizes an object as its instance. Used by instanceof. |
Symbol.isConcatSpreadable | A boolean value indicating if an object should be flattened to its array elements when using Array.prototype.concat. |
Symbol.iterator | A method that returns the default iterator for an object. Used by for...of. |
Symbol.match | A method that matches against a string. Used by String.prototype.match. |
Symbol.replace | A method that replaces matched substrings of a string. Used by String.prototype.replace. |
Symbol.search | A method that returns the index within a string that matches the regular expression. Used by String.prototype.search. |
Symbol.split | A method that splits a string at the indices that match a regular expression. Used by String.prototype.split. |
Symbol.toPrimitive | A method that converts an object to a primitive value. |
Symbol.toStringTag | A string value used for the default description of an object, e.g., [object MyObject]. |
Sources: Jurassic/Library/Symbol/SymbolConstructor.cs:42-113, Test Suite Runner/Kangax/compat-table.json:11786-12625
The file RegExpInstance.g.cs shows a concrete implementation of several well-known symbols on the RegExpInstance prototype, allowing regular expressions to customize their behavior with String methods.
// From Jurassic/Library/RegExp/RegExpInstance.g.cs
private static List<PropertyNameAndValue> GetDeclarativeProperties(ScriptEngine engine)
{
return new List<PropertyNameAndValue>(12)
{
// ... other properties
new PropertyNameAndValue(Symbol.Match, new ClrStubFunction(engine, "[Symbol.match]", 1, __STUB__Match), PropertyAttributes.NonEnumerable),
new PropertyNameAndValue(Symbol.Replace, new ClrStubFunction(engine, "[Symbol.replace]", 2, __STUB__Replace), PropertyAttributes.NonEnumerable),
new PropertyNameAndValue(Symbol.Search, new ClrStubFunction(engine, "[Symbol.search]", 1, __STUB__Search), PropertyAttributes.NonEnumerable),
new PropertyNameAndValue(Symbol.Split, new ClrStubFunction(engine, "[Symbol.split]", 2, __STUB__Split), PropertyAttributes.NonEnumerable),
// ... other properties
};
}
Sources: Jurassic/Library/RegExp/RegExpInstance.g.cs:17-26
Conclusion
The Jurassic project provides a robust and spec-compliant implementation of the ES6 Symbol feature. This includes the Symbol primitive type for creating unique identifiers, the Symbol constructor object with its static methods for managing a global symbol registry (Symbol.for, Symbol.keyFor), and a wide array of well-known symbols that enable deep customization of object behavior. This functionality is crucial for running modern JavaScript code that relies on these metaprogramming capabilities within the .NET ecosystem.
Sources: README.md, Jurassic/Library/Symbol/Symbol.cs, Jurassic/Library/Symbol/SymbolConstructor.cs