Symbol
# Symbol
# Overview
In ES5, object property names are all strings, which can easily lead to property name conflicts. For example, if you use an object provided by someone else but want to add new methods to it (mixin pattern), the name of the new method might conflict with existing methods. It would be great if there were a mechanism to guarantee that every property name is unique, fundamentally preventing property name conflicts. This is the reason ES6 introduced Symbol.
ES6 introduced a new primitive data type Symbol, which represents a unique value. It is the seventh data type in JavaScript, the previous six being: undefined, null, Boolean, String, Number, and Object.
Symbol values are generated through the Symbol function. This means that object property names can now be of two types: the original string type, and the new Symbol type. Any property name that is of the Symbol type is guaranteed to be unique and will not conflict with other property names.
let s = Symbol();
typeof s
// "symbol"
2
3
4
In the code above, the variable s is a unique value. The result of the typeof operator shows that variable s is of the Symbol data type, not a string or any other type.
Note that the Symbol function cannot be used with the new command, otherwise an error will be thrown. This is because the generated Symbol is a primitive type value, not an object. In other words, since Symbol values are not objects, properties cannot be added to them. Essentially, it is a data type similar to strings.
The Symbol function can accept a string as an argument, which serves as a description of the Symbol instance, mainly for display in the console or for easier identification when converted to a string.
let s1 = Symbol('foo');
let s2 = Symbol('bar');
s1 // Symbol(foo)
s2 // Symbol(bar)
s1.toString() // "Symbol(foo)"
s2.toString() // "Symbol(bar)"
2
3
4
5
6
7
8
In the code above, s1 and s2 are two Symbol values. Without arguments, their output in the console would both be Symbol(), making them hard to distinguish. With arguments, descriptions are added to them, making it clear which value is which in the output.
If the argument to Symbol is an object, the object's toString method will be called to convert it to a string before generating a Symbol value.
const obj = {
toString() {
return 'abc';
}
};
const sym = Symbol(obj);
sym // Symbol(abc)
2
3
4
5
6
7
Note that the argument to the Symbol function is only a description of the current Symbol value, so Symbol functions with the same argument will return unequal values.
// Without arguments
let s1 = Symbol();
let s2 = Symbol();
s1 === s2 // false
// With arguments
let s1 = Symbol('foo');
let s2 = Symbol('foo');
s1 === s2 // false
2
3
4
5
6
7
8
9
10
11
In the code above, s1 and s2 are both return values of the Symbol function with the same argument, but they are not equal.
Symbol values cannot be used in operations with other types of values; doing so will throw an error.
let sym = Symbol('My symbol');
"your symbol is " + sym
// TypeError: can't convert symbol to string
`your symbol is ${sym}`
// TypeError: can't convert symbol to string
2
3
4
5
6
However, Symbol values can be explicitly converted to strings.
let sym = Symbol('My symbol');
String(sym) // 'Symbol(My symbol)'
sym.toString() // 'Symbol(My symbol)'
2
3
4
Additionally, Symbol values can be converted to booleans, but cannot be converted to numbers.
let sym = Symbol();
Boolean(sym) // true
!sym // false
if (sym) {
// ...
}
Number(sym) // TypeError
sym + 2 // TypeError
2
3
4
5
6
7
8
9
10
# Symbol.prototype.description
When creating a Symbol, you can add a description.
const sym = Symbol('foo');
In the code above, the description of sym is the string foo.
However, reading this description requires explicitly converting the Symbol to a string, as shown below.
const sym = Symbol('foo');
String(sym) // "Symbol(foo)"
sym.toString() // "Symbol(foo)"
2
3
4
The usage above is not very convenient. ES2019 (opens new window) provides an instance property description that directly returns the Symbol's description.
const sym = Symbol('foo');
sym.description // "foo"
2
3
# Symbol as Property Names
Since every Symbol value is unique, Symbol values can be used as identifiers for object property names, guaranteeing that no same-named properties will appear. This is very useful when an object is composed of multiple modules, preventing a key from being accidentally overwritten.
let mySymbol = Symbol();
// First approach
let a = {};
a[mySymbol] = 'Hello!';
// Second approach
let a = {
[mySymbol]: 'Hello!'
};
// Third approach
let a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });
// All approaches produce the same result
a[mySymbol] // "Hello!"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
The code above uses bracket notation and Object.defineProperty to set an object's property name to a Symbol value.
Note that the dot operator cannot be used when a Symbol value is used as a property name.
const mySymbol = Symbol();
const a = {};
a.mySymbol = 'Hello!';
a[mySymbol] // undefined
a['mySymbol'] // "Hello!"
2
3
4
5
6
In the code above, since the dot operator is always followed by a string, it does not read mySymbol as the identifier it refers to. As a result, a's property name is actually a string, not a Symbol value.
Similarly, when using a Symbol value to define a property inside an object, the Symbol value must be placed in brackets.
let s = Symbol();
let obj = {
[s]: function (arg) { ... }
};
obj[s](123);
2
3
4
5
6
7
In the code above, if s is not placed in brackets, the property's key would be the string s, not the Symbol value that s represents.
Using the enhanced object literal syntax, the obj object in the code above can be written more concisely.
let obj = {
[s](arg) { ... }
};
2
3
The Symbol type can also be used to define a set of constants, ensuring that the values of all constants are unique.
const log = {};
log.levels = {
DEBUG: Symbol('debug'),
INFO: Symbol('info'),
WARN: Symbol('warn')
};
console.log(log.levels.DEBUG, 'debug message');
console.log(log.levels.INFO, 'info message');
2
3
4
5
6
7
8
9
Here is another example.
const COLOR_RED = Symbol();
const COLOR_GREEN = Symbol();
function getComplement(color) {
switch (color) {
case COLOR_RED:
return COLOR_GREEN;
case COLOR_GREEN:
return COLOR_RED;
default:
throw new Error('Undefined color');
}
}
2
3
4
5
6
7
8
9
10
11
12
13
The greatest benefit of using Symbol values for constants is that no other value can ever have the same value, ensuring that the switch statement above works as designed.
One more thing to note: when a Symbol value is used as a property name, that property is still a public property, not a private one.
# Example: Eliminating Magic Strings
A magic string is a specific string or number that appears multiple times in code and is strongly coupled with the code. Well-styled code should eliminate magic strings as much as possible, replacing them with clearly named variables.
function getArea(shape, options) {
let area = 0;
switch (shape) {
case 'Triangle': // magic string
area = .5 * options.width * options.height;
break;
/* ... more code ... */
}
return area;
}
getArea('Triangle', { width: 100, height: 100 }); // magic string
2
3
4
5
6
7
8
9
10
11
12
13
14
In the code above, the string Triangle is a magic string. It appears multiple times and is "strongly coupled" with the code, making future modifications and maintenance difficult.
Note:
The
Trianglestring appears multiple times in the code. If you want to change this string later, you would need to modify many places, making maintenance difficult.
A common method for eliminating magic strings is to assign them to variables.
const shapeType = {
triangle: 'Triangle'
};
function getArea(shape, options) {
let area = 0;
switch (shape) {
case shapeType.triangle:
area = .5 * options.width * options.height;
break;
}
return area;
}
getArea(shapeType.triangle, { width: 100, height: 100 });
2
3
4
5
6
7
8
9
10
11
12
13
14
15
In the code above, we assigned Triangle to the triangle property of the shapeType object, thus eliminating the strong coupling.
Upon careful analysis, you can see that the exact value of shapeType.triangle doesn't matter, as long as it doesn't conflict with other shapeType property values. Therefore, this is a perfect case for using Symbol values.
const shapeType = {
triangle: Symbol()
};
2
3
In the code above, aside from changing the value of shapeType.triangle to a Symbol, no other modifications are needed.
# Traversal of Property Names
When Symbol is used as a property name, the property will not appear in for...in or for...of loops, nor will it be returned by Object.keys(), Object.getOwnPropertyNames(), or JSON.stringify().
However, it is not a private property either. The Object.getOwnPropertySymbols() method can retrieve all Symbol property names of a given object. This method returns an array whose elements are all the Symbol values used as property names on the current object.
const obj = {};
let a = Symbol('a');
let b = Symbol('b');
obj[a] = 'Hello';
obj[b] = 'World';
const objectSymbols = Object.getOwnPropertySymbols(obj);
objectSymbols
// [Symbol(a), Symbol(b)]
2
3
4
5
6
7
8
9
10
11
The code above is an example of the Object.getOwnPropertySymbols() method, which can retrieve all Symbol property names.
Below is another example, comparing Object.getOwnPropertySymbols() with the for...in loop and the Object.getOwnPropertyNames method.
const obj = {};
const foo = Symbol('foo');
obj[foo] = 'bar';
for (let i in obj) {
console.log(i); // no output
}
Object.getOwnPropertyNames(obj) // []
Object.getOwnPropertySymbols(obj) // [Symbol(foo)]
2
3
4
5
6
7
8
9
10
11
In the code above, neither the for...in loop nor Object.getOwnPropertyNames() can retrieve Symbol key names; you need to use Object.getOwnPropertySymbols().
Another new API, Reflect.ownKeys(), can return all types of key names, including both regular key names and Symbol key names.
let obj = {
[Symbol('my_key')]: 1,
enum: 2,
nonEnum: 3
};
Reflect.ownKeys(obj)
// ["enum", "nonEnum", Symbol(my_key)]
2
3
4
5
6
7
8
Since Symbol-keyed properties are not found by conventional traversal methods, we can take advantage of this feature to define properties that are not private but are intended for internal use only.
let size = Symbol('size');
class Collection {
constructor() {
this[size] = 0;
}
add(item) {
this[this[size]] = item;
this[size]++;
}
static sizeOf(instance) {
return instance[size];
}
}
let x = new Collection();
Collection.sizeOf(x) // 0
x.add('foo');
Collection.sizeOf(x) // 1
Object.keys(x) // ['0']
Object.getOwnPropertyNames(x) // ['0']
Object.getOwnPropertySymbols(x) // [Symbol(size)]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
In the code above, the size property of object x is a Symbol value, so Object.keys(x) and Object.getOwnPropertyNames(x) cannot retrieve it. This creates the effect of a non-private internal method.
# Symbol.for(), Symbol.keyFor()
Sometimes we want to reuse the same Symbol value. The Symbol.for() method can achieve this. It accepts a string as an argument, then searches for a Symbol value with that string as its name. If found, it returns that Symbol value; otherwise, it creates a new Symbol value with that string as its name and registers it globally.
let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');
s1 === s2 // true
2
3
4
In the code above, both s1 and s2 are Symbol values generated by Symbol.for with the same argument, so they are actually the same value.
Both Symbol.for() and Symbol() generate new Symbols. The difference is that the former is registered in a global environment for searching, while the latter is not. Symbol.for() does not return a new Symbol value every time it's called; instead, it first checks whether the given key already exists, and only creates a new value if it doesn't. For example, calling Symbol.for("cat") 30 times will always return the same Symbol value, but calling Symbol("cat") 30 times will return 30 different Symbol values.
Symbol.for("bar") === Symbol.for("bar")
// true
Symbol("bar") === Symbol("bar")
// false
2
3
4
5
In the code above, since Symbol() has no registration mechanism, each call returns a different value.
The Symbol.keyFor() method returns the key of a registered Symbol value.
let s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"
let s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined
2
3
4
5
In the code above, variable s2 is an unregistered Symbol value, so undefined is returned.
Note that Symbol.for() registers the name for a Symbol value in the global environment, regardless of whether it is run in the global environment.
function foo() {
return Symbol.for('bar');
}
const x = foo();
const y = Symbol.for('bar');
console.log(x === y); // true
2
3
4
5
6
7
In the code above, Symbol.for('bar') runs inside a function, but the generated Symbol value is registered in the global environment. So the second call to Symbol.for('bar') can retrieve this Symbol value.
This global registration feature of Symbol.for() can be used to get the same value across different iframes or service workers.
iframe = document.createElement('iframe');
iframe.src = String(window.location);
document.body.appendChild(iframe);
iframe.contentWindow.Symbol.for('foo') === Symbol.for('foo')
// true
2
3
4
5
6
In the code above, the Symbol value generated by the iframe window can be obtained in the main page.
# Example: Module Singleton Pattern
The Singleton pattern means that calling a class always returns the same instance.
For Node, a module file can be thought of as a class. How can you ensure that every time this module file is executed, the same instance is returned?
An easy approach is to place the instance on the top-level object global.
// mod.js
function A() {
this.foo = 'hello';
}
if (!global._foo) {
global._foo = new A();
}
module.exports = global._foo;
2
3
4
5
6
7
8
9
10
Then, load the mod.js above.
const a = require('./mod.js');
console.log(a.foo);
2
In the code above, variable a always loads the same instance of A.
However, there is a problem: the global variable global._foo is writable, and any file can modify it.
global._foo = { foo: 'world' };
const a = require('./mod.js');
console.log(a.foo);
2
3
4
The code above would cause all scripts that load mod.js to get incorrect results.
To prevent this, we can use Symbol.
// mod.js
const FOO_KEY = Symbol.for('foo');
function A() {
this.foo = 'hello';
}
if (!global[FOO_KEY]) {
global[FOO_KEY] = new A();
}
module.exports = global[FOO_KEY];
2
3
4
5
6
7
8
9
10
11
12
In the code above, global[FOO_KEY] is protected from accidental overwriting, but it can still be deliberately modified.
global[Symbol.for('foo')] = { foo: 'world' };
const a = require('./mod.js');
2
3
If the key is generated using the Symbol method, then external code cannot reference this value and therefore cannot modify it.
// mod.js
const FOO_KEY = Symbol('foo');
// The rest of the code is the same...
2
3
4
The code above makes it impossible for other scripts to reference FOO_KEY. However, this also means that if the script is executed multiple times, each execution will produce a different FOO_KEY. Although Node caches script execution results and normally won't execute the same script multiple times, users can manually clear the cache, so it's not absolutely reliable.
# Built-in Symbol Values
In addition to Symbols defined for your own use, ES6 provides 11 built-in Symbol values that point to methods used internally by the language.
# Symbol.hasInstance
An object's Symbol.hasInstance property points to an internal method. When other objects use the instanceof operator to check if they are instances of this object, this method is called. For example, foo instanceof Foo internally calls Foo[Symbol.hasInstance](foo).
class MyClass {
[Symbol.hasInstance](foo) {
return foo instanceof Array;
}
}
[1, 2, 3] instanceof new MyClass() // true
2
3
4
5
6
7
In the code above, MyClass is a class. new MyClass() returns an instance. The instance's Symbol.hasInstance method is automatically called during instanceof operations to determine if the left-hand operand is an instance of Array.
Here is another example.
class Even {
static [Symbol.hasInstance](obj) {
return Number(obj) % 2 === 0;
}
}
// Equivalent to
const Even = {
[Symbol.hasInstance](obj) {
return Number(obj) % 2 === 0;
}
};
1 instanceof Even // false
2 instanceof Even // true
12345 instanceof Even // false
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Symbol.isConcatSpreadable
An object's Symbol.isConcatSpreadable property is a boolean value indicating whether the object can be spread when used with Array.prototype.concat().
let arr1 = ['c', 'd'];
['a', 'b'].concat(arr1, 'e') // ['a', 'b', 'c', 'd', 'e']
arr1[Symbol.isConcatSpreadable] // undefined
let arr2 = ['c', 'd'];
arr2[Symbol.isConcatSpreadable] = false;
['a', 'b'].concat(arr2, 'e') // ['a', 'b', ['c','d'], 'e']
2
3
4
5
6
7
The code above shows that the default behavior of arrays is to be spreadable. Symbol.isConcatSpreadable defaults to undefined. When this property is set to true, the spreading effect is the same.
Array-like objects behave the opposite way -- they are not spread by default. Their Symbol.isConcatSpreadable property must be set to true for them to be spread.
let obj = {length: 2, 0: 'c', 1: 'd'};
['a', 'b'].concat(obj, 'e') // ['a', 'b', obj, 'e']
obj[Symbol.isConcatSpreadable] = true;
['a', 'b'].concat(obj, 'e') // ['a', 'b', 'c', 'd', 'e']
2
3
4
5
The Symbol.isConcatSpreadable property can also be defined inside a class.
class A1 extends Array {
constructor(args) {
super(args);
this[Symbol.isConcatSpreadable] = true;
}
}
class A2 extends Array {
constructor(args) {
super(args);
}
get [Symbol.isConcatSpreadable] () {
return false;
}
}
let a1 = new A1();
a1[0] = 3;
a1[1] = 4;
let a2 = new A2();
a2[0] = 5;
a2[1] = 6;
[1, 2].concat(a1).concat(a2)
// [1, 2, 3, 4, [5, 6]]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
In the code above, class A1 is spreadable while class A2 is not, which leads to different results when using concat.
Note the difference in where Symbol.isConcatSpreadable is defined: A1 defines it on the instance, while A2 defines it on the class itself, yet the effect is the same.
# Symbol.species
An object's Symbol.species property points to a constructor function. When creating derived objects, this constructor will be used.
class MyArray extends Array {
}
const a = new MyArray(1, 2, 3);
const b = a.map(x => x);
const c = a.filter(x => x > 1);
b instanceof MyArray // true
c instanceof MyArray // true
2
3
4
5
6
7
8
9
In the code above, the subclass MyArray extends the parent class Array. a is an instance of MyArray, and b and c are objects derived from a. You might think that b and c should be arrays (instances of Array) since they are generated by array methods, but they are actually also instances of MyArray.
The Symbol.species property is provided to solve this problem. Now we can set the Symbol.species property for MyArray.
class MyArray extends Array {
static get [Symbol.species]() { return Array; }
}
2
3
In the code above, because Symbol.species is defined, the function returned by this property will be used as the constructor when creating derived objects. This example also shows that Symbol.species should be defined using a get accessor. The default Symbol.species property is equivalent to the following.
static get [Symbol.species]() {
return this;
}
2
3
Now, let's revisit the previous example.
class MyArray extends Array {
static get [Symbol.species]() { return Array; }
}
const a = new MyArray();
const b = a.map(x => x);
b instanceof MyArray // false
b instanceof Array // true
2
3
4
5
6
7
8
9
In the code above, the derived object generated by a.map(x => x) is not an instance of MyArray, but directly an instance of Array.
Here is another example.
class T1 extends Promise {
}
class T2 extends Promise {
static get [Symbol.species]() {
return Promise;
}
}
new T1(r => r()).then(v => v) instanceof T1 // true
new T2(r => r()).then(v => v) instanceof T2 // false
2
3
4
5
6
7
8
9
10
11
In the code above, T2 defines a Symbol.species property while T1 does not. The result is that when creating derived objects (via the then method), T1 calls its own constructor while T2 calls Promise's constructor.
In summary, the purpose of Symbol.species is that when an instance object needs to call its own constructor again during execution, it will use the constructor specified by this property. Its main use case is when class libraries are modified versions of a base class, and the author may want inherited methods to return instances of the base class rather than the subclass.
# Symbol.match
An object's Symbol.match property points to a function. When str.match(myObject) is executed, if this property exists, it will be called and its return value will be returned.
String.prototype.match(regexp)
// Equivalent to
regexp[Symbol.match](this)
class MyMatcher {
[Symbol.match](string) {
return 'hello world'.indexOf(string);
}
}
'e'.match(new MyMatcher()) // 1
2
3
4
5
6
7
8
9
10
11
# Symbol.replace
An object's Symbol.replace property points to a method. When the object is called by String.prototype.replace, this method's return value is returned.
String.prototype.replace(searchValue, replaceValue)
// Equivalent to
searchValue[Symbol.replace](this, replaceValue)
2
3
Here is an example.
const x = {};
x[Symbol.replace] = (...s) => console.log(s);
'Hello'.replace(x, 'World') // ["Hello", "World"]
2
3
4
The Symbol.replace method receives two arguments: the first is the object on which the replace method is acting (in the example above, Hello), and the second is the replacement value (in the example above, World).
# Symbol.search
An object's Symbol.search property points to a method. When the object is called by String.prototype.search, this method's return value is returned.
String.prototype.search(regexp)
// Equivalent to
regexp[Symbol.search](this)
class MySearch {
constructor(value) {
this.value = value;
}
[Symbol.search](string) {
return string.indexOf(this.value);
}
}
'foobar'.search(new MySearch('foo')) // 0
2
3
4
5
6
7
8
9
10
11
12
13
# Symbol.split
An object's Symbol.split property points to a method. When the object is called by String.prototype.split, this method's return value is returned.
String.prototype.split(separator, limit)
// Equivalent to
separator[Symbol.split](this, limit)
2
3
Here is an example.
class MySplitter {
constructor(value) {
this.value = value;
}
[Symbol.split](string) {
let index = string.indexOf(this.value);
if (index === -1) {
return string;
}
return [
string.substr(0, index),
string.substr(index + this.value.length)
];
}
}
'foobar'.split(new MySplitter('foo'))
// ['', 'bar']
'foobar'.split(new MySplitter('bar'))
// ['foo', '']
'foobar'.split(new MySplitter('baz'))
// 'foobar'
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
The method above uses Symbol.split to redefine the behavior of the string object's split method.
# Symbol.iterator
An object's Symbol.iterator property points to the object's default iterator method.
const myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable] // [1, 2, 3]
2
3
4
5
6
7
8
When an object is iterated with a for...of loop, the Symbol.iterator method is called, returning the object's default iterator. For details, see the "Iterator and for...of Loop" chapter.
class Collection {
*[Symbol.iterator]() {
let i = 0;
while(this[i] !== undefined) {
yield this[i];
++i;
}
}
}
let myCollection = new Collection();
myCollection[0] = 1;
myCollection[1] = 2;
for(let value of myCollection) {
console.log(value);
}
// 1
// 2
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Symbol.toPrimitive
An object's Symbol.toPrimitive property points to a method. When the object is converted to a primitive type value, this method is called, returning the corresponding primitive type value of the object.
Symbol.toPrimitive receives a string argument indicating the current operation mode. There are three modes:
- Number: the context requires conversion to a number
- String: the context requires conversion to a string
- Default: the context can convert to either a number or a string
let obj = {
[Symbol.toPrimitive](hint) {
switch (hint) {
case 'number':
return 123;
case 'string':
return 'str';
case 'default':
return 'default';
default:
throw new Error();
}
}
};
2 * obj // 246
3 + obj // '3default'
obj == 'default' // true
String(obj) // 'str'
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Symbol.toStringTag
An object's Symbol.toStringTag property points to a method. When Object.prototype.toString is called on the object and this property exists, its return value will appear in the string returned by toString, representing the object's type. In other words, this property can be used to customize the string after object in [object Object] or [object Array].
// Example 1
({[Symbol.toStringTag]: 'Foo'}.toString())
// "[object Foo]"
// Example 2
class Collection {
get [Symbol.toStringTag]() {
return 'xxx';
}
}
let x = new Collection();
Object.prototype.toString.call(x) // "[object xxx]"
2
3
4
5
6
7
8
9
10
11
12
The Symbol.toStringTag property values for built-in objects added in ES6 are as follows:
JSON[Symbol.toStringTag]: 'JSON'Math[Symbol.toStringTag]: 'Math'- Module object
M[Symbol.toStringTag]: 'Module' ArrayBuffer.prototype[Symbol.toStringTag]: 'ArrayBuffer'DataView.prototype[Symbol.toStringTag]: 'DataView'Map.prototype[Symbol.toStringTag]: 'Map'Promise.prototype[Symbol.toStringTag]: 'Promise'Set.prototype[Symbol.toStringTag]: 'Set'%TypedArray%.prototype[Symbol.toStringTag]: 'Uint8Array', etc.WeakMap.prototype[Symbol.toStringTag]: 'WeakMap'WeakSet.prototype[Symbol.toStringTag]: 'WeakSet'%MapIteratorPrototype%[Symbol.toStringTag]: 'Map Iterator'%SetIteratorPrototype%[Symbol.toStringTag]: 'Set Iterator'%StringIteratorPrototype%[Symbol.toStringTag]: 'String Iterator'Symbol.prototype[Symbol.toStringTag]: 'Symbol'Generator.prototype[Symbol.toStringTag]: 'Generator'GeneratorFunction.prototype[Symbol.toStringTag]: 'GeneratorFunction'
# Symbol.unscopables
An object's Symbol.unscopables property points to an object. This object specifies which properties are excluded from the with environment when the with keyword is used.
Array.prototype[Symbol.unscopables]
// {
// copyWithin: true,
// entries: true,
// fill: true,
// find: true,
// findIndex: true,
// includes: true,
// keys: true
// }
Object.keys(Array.prototype[Symbol.unscopables])
// ['copyWithin', 'entries', 'fill', 'find', 'findIndex', 'includes', 'keys']
2
3
4
5
6
7
8
9
10
11
12
13
The code above shows that arrays have 7 properties that are excluded by the with statement.
// Without unscopables
class MyClass {
foo() { return 1; }
}
var foo = function () { return 2; };
with (MyClass.prototype) {
foo(); // 1
}
// With unscopables
class MyClass {
foo() { return 1; }
get [Symbol.unscopables]() {
return { foo: true };
}
}
var foo = function () { return 2; };
with (MyClass.prototype) {
foo(); // 2
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
The code above specifies the Symbol.unscopables property, which prevents the with block from looking for the foo property in the current scope, so foo will refer to the variable in the outer scope.