ES6 Symbol

Introduction

ES5 object property names are all strings, which is prone to property name conflicts. For example, if you use an object provided by someone else, but want to add a new method (mixin mode) to this object, the name of the new method may conflict with the existing method. It would be nice if there was a mechanism to ensure that the name of each property is unique, which fundamentally prevents property name conflicts. This is why Symbols were introduced in ES6.

ES6 introduces a new original data source type Symbol that represents a unique value. It is the seventh data type in the JavaScript language, after the first six: undefined, null, Boolean, string, number, and object.

Symbol values are generated by the Symbol function. This means that object property names can now have two types, one is the original string, and the other is the new Symbol type. Any property name that belongs to the Symbol type is unique and can be guaranteed not to conflict with other property names.

Features and applications

Unique

1
2
3
4
let s = Symbol();

typeof s
// "symbol"

In the above code, the variable s is a unique value. The result of the typeof operator indicates that the variable s is a Symbol data type, not some other type such as string.

Note that the’new ‘command cannot be used before the’Symbol’ function, otherwise an error will be reported. This is because the generated Symbol is a primitive type value, not an object. That is, since the Symbol value is not an object, properties cannot be added. Basically, it is a data type similar to string.

The’Symbol 'function can accept a string as a parameter, representing a description of the Symbol instance, mainly for display in the Console, or when converted to string, it is easier to distinguish.

1
2
3
4
5
6
7
8
let s1 = Symbol('foo');
let s2 = Symbol('bar');

s1 // Symbol(foo)
s2 // Symbol(bar)

s1.toString() // "Symbol(foo)"
s2.toString() // "Symbol(bar)"

In the above code, ‘s1’ and’s2 'are two Symbol values. If no parameters are added, their output in the Console is’Symbol () ', which is not conducive to distinction. After having parameters, it is equivalent to adding a description to them, and when outputting, it can be distinguished which value it is.

If the parameter of the symbol is an object, the’toString 'method of the object will be called, converted to a string, and then a symbol value will be generated.

1
2
3
4
5
6
7
const obj = {
toString() {
return 'abc';
}
};
const sym = Symbol(obj);
sym // Symbol(abc)

** Note that the parameters of’Symbol ‘function only represent a description of the current symbol value, so the return value of’Symbol’ function with the same parameter is not equal. **

1
2
3
4
5
6
7
8
9
10
11
If there are no parameters
let s1 = Symbol();
let s2 = Symbol();

s1 = s2 // false

//In the case of parameters
let s1 = Symbol('foo');
let s2 = Symbol('foo');

s1 = s2 // false

In the above code, ‘s1’ and’s2 ‘are both return values of’Symbol’ function and have the same parameters, but they are not equal.

Symbol values cannot be calculated with values of other types and an error will be reported.

1
2
3
4
5
6
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

Symbol values, however, can be explicitly converted to strings.

1
2
3
4
let sym = Symbol('My symbol');

String(sym) // 'Symbol(My symbol)'
sym.toString() // 'Symbol(My symbol)'

Symbol values can also be converted to Boolean values, but not to numeric values.

1
2
3
4
5
6
7
8
9
10
let sym = Symbol();
Boolean(sym) // true
!sym // false

if (sym) {
// ...
}

Number(sym) // TypeError
sym + 2 // TypeError

Application

Since each symbol value is not equal, this means that the symbol value can be used as an identifier for the property name of the object, ensuring that no property with the same name will appear. This is very useful in cases where an object consists of multiple modules, preventing a key from being accidentally overwritten or overwritten.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let mySymbol = Symbol();

//The first way of writing
let a = {};
a[mySymbol] = 'Hello!';

//The second way of writing
let a = {
[mySymbol]: 'Hello!'
};

//The third way of writing
let a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });

//The above writing methods all get the same result
a[mySymbol] // "Hello!"

The above code specifies the property name of the object as a Symbol value through the square bracket structure and’Object.defineProperty '.

Note that the dot operator cannot be used when a Symbol value is used as an object property name.

1
2
3
4
5
6
const mySymbol = Symbol();
const a = {};

a.mySymbol = 'Hello!';
a[mySymbol] // undefined
a['mySymbol'] // "Hello!"

In the above code, because the dot operator is always followed by a string, the value referred to by’mySymbol ‘as the identifier is not read, resulting in the attribute name of’a’ actually being a string instead of a Symbol value.

Similarly, inside an object, when defining a property with a Symbol value, the Symbol value must be enclosed in square brackets.

1
2
3
4
5
6
7
let s = Symbol();

let obj = {
[s]: function (arg) { ... }
};

obj[s](123);

In the above code, if s is not placed in square brackets, the key name of the property is the string s, not the Symbol value represented by s.

Application

Symbol types can also be used to define a set of constants whose values are guaranteed to be unequal.

1
2
3
4
5
6
7
8
9
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');

Here’s another example.

1
2
3
4
5
6
7
8
9
10
11
12
13
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');
}
}

The biggest benefit of using symbol values for constants is that no other value can have the same value, so you can guarantee that the’switch 'statement above will work as designed.

Cannot be traversed

Symbol as a property name, when traversing the object, the property will not appear in the for… in, for… of loops, nor will it be returned by Object.keys (), Object.getOwnPropertyNames (), JSON.stringify ().

However, it is also not a private property, there is a’Object.getOwnPropertySymbols () 'method that can get all the Symbol property names of the specified object. This method returns an array of all Symbol values used as property names for the current object.

1
2
3
4
5
6
7
8
9
10
11
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)]

The above code is an example of the Object.getOwnPropertySymbols () method, which can obtain all Symbol property names.

Here is another example of how the Object.getOwnPropertySymbols () method compares to the for… in loop and the Object.getOwnPropertyNames method.

1
2
3
4
5
6
7
8
9
10
11
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)]

In the above code, using the “for… in” loop and the "Object.getOwnPropertyNames () " method do not get the Symbol key name, you need to use the "Object.getOwnPropertySymbols () " method.

Another new API, the Reflect.ownKeys () method can return all types of key names, including regular key names and Symbol key names.

1
2
3
4
5
6
7
8
let obj = {
[Symbol('my_key')]: 1,
enum: 2,
nonEnum: 3
};

Reflect.ownKeys(obj)
// ["enum", "nonEnum", Symbol(my_key)]

Application

Due to the Symbol value as the key name, it will not be traversed by regular methods. We can take advantage of this feature to define some non-private methods for objects that we want to use only internally.

1
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
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)]

In the above code, the size property of the object x is a Symbol value, so neither Object.keys (x) nor Object.getOwnPropertyNames (x) can get it. This creates the effect of a non-private internal method.

Creating the Same Symbol

Sometimes, we want to reuse the same symbol value, and the’Symbol.for () 'method can do this. It takes a string as a parameter and searches for a symbol value with that parameter as its name. If there is, return the Symbol value, otherwise create a new Symbol value with that string name and register it globally.

1
2
3
4
let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');

s1 = s2 // true

In the above code, ‘s1’ and’s2 ‘are both Symbol values, but they are both generated by the’Symbol.for’ method with the same parameters, so they are actually the same value.

Both’Symbol.for () 'and’Symbol () ’ will generate new symbols. The difference between them is that the former will be registered for search in the global environment, while the latter will not. 'Symbol.for () ’ does not return a new value of type Symbol every time it is called, but will first check if the given’key 'already exists, and if not, will create a new value. For example, if you call’Symbol.for (“cat”) '30 times, it will return the same symbol value each time, but calling’Symbol (“cat”) ’ 30 times will return 30 different symbol values.

1
2
3
4
5
Symbol.for("bar") = Symbol.for("bar")
// true

Symbol("bar") = Symbol("bar")
// false

In the above code, since there is no registration mechanism for’Symbol () 'writing, each call will return a different value.

The Symbol.keyFor () method returns the key of a registered Symbol value.

1
2
3
4
5
let s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"

let s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined

In the above code, the variable’s2 ‘belongs to an unregistered Symbol value, so it returns’undefined’.

Note that Symbol.for () registers names for Symbol values that are global, regardless of whether they are running globally.

1
2
3
4
5
6
7
function foo() {
return Symbol.for('bar');
}

const x = foo();
const y = Symbol.for('bar');
console.log(x = y); // true

In the above code, ‘Symbol.for (’ bar ') ’ is run internally by function, but the generated symbol value is registered in the global environment. Therefore, the second run of’Symbol.for (‘bar’) 'can get this Symbol value.

This global registration feature of Symbol.for () can be used to retrieve the same value in different iframes or service workers.

1
2
3
4
5
6
iframe = document.createElement('iframe');
iframe.src = String(window.location);
document.body.appendChild(iframe);

iframe.contentWindow.Symbol.for('foo') = Symbol.for('foo')
// true

In the above code, the symbol value generated by the iframe window can be obtained on the main page.

Built-in Symbols

Symbol.hasInstance

The Symbol.hasInstance property of the object points to an internal method. This method is called when other objects use the instanceof operator to determine whether they are instances of the object. For example, ‘foo instanceof Foo’ is actually called’Foo 'internally in the language.Symbol.hasInstance`。

1
2
3
4
5
6
7
class MyClass {
[Symbol.hasInstance](foo) {
return foo instanceof Array;
}
}

[1, 2, 3] instanceof new MyClass() // true

In the above code, ‘MyClass’ is a class, and 'new MyClass () ’ will return an instance. The’Symbol.hasInstance ‘method of this instance will be automatically called during the’instanceof’ operation to determine whether the operator on the left is an instance of’Array '.

Here’s another example.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

Symbol.isConcatSpreadable

The Symbol.isConcatSpreadable property of the object is equal to a Boolean value indicating whether the object can be expanded when used with Array.prototype.concat ().

1
2
3
4
5
6
7
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']

The above code explains that the default behavior of the array is that it can be expanded, and’Symbol.isConcatSpreadable ‘defaults to’undefined’. When this property is equal to’true ', it also has the effect of expansion.

Array-like objects are the opposite and are not expanded by default. Its’Symbol.isConcatSpreadable ‘property is set to’true’ before it can be expanded.

1
2
3
4
5
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']

The Symbol.isConcatSpreadable property can also be defined inside a class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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]]

In the above code, the class’A1 ‘is expandable, and the class’A2’ is not expandable, so using’concat 'has different results.

Note that the difference in the position of Symbol.isConcatSpreadable, A1 is defined on the instance, A2 is defined on the class itself, the effect is the same.

Reference materials:

https://es6.ruanyifeng.com/#docs/symbol