An overview of the

Object attribute names in ES5 are all strings, which can easily cause attribute name conflicts. For example, if you use an object provided by someone else and want to add a new method to the object (the mixin pattern), the name of the new method may conflict with the existing method. It would be nice if there were a mechanism to ensure that each property name was unique, preventing property name conflicts at all. This is why ES6 introduced Symbol.

ES6 introduces a new primitive data type, Symbol, that represents unique values. It is the seventh data type in JavaScript, with undefined, NULL, Boolean, String, Number, and Object being the first six.

The Symbol value is generated by the Symbol function. This means that object property names can now be of two types: the original string and the new Symbol type. Attribute names that belong to the Symbol type are unique and are guaranteed not to conflict with other attribute names.

let s = Symbol();

typeof s
// "symbol"
Copy the code

In the code above, the variable S is a unique value. The result of the Typeof operator, indicating that the variable S is a Symbol data type and not some other type, such as a 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, because the Symbol value is not an object, attributes cannot be added. Basically, it’s a data type similar to a string.

The Symbol function can take a string as an argument representing a description of the Symbol instance, mainly to make it easier to distinguish when displayed on the console or 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)"
Copy the code

In the code above, s1 and s2 are two symbols. Without arguments, they are both printed as Symbol() on the console, making it hard to tell apart. So when you have parameters, you have descriptions for them, and when you print them out, you can tell which values they are.

If Symbol’s argument is an object, the object’s toString method is called and converted to a string before a Symbol value is generated.

const obj = { toString() { return 'abc'; }}; const sym = Symbol(obj); sym // Symbol(abc)Copy the code

Note that the parameter to the Symbol function only represents a description of the current Symbol value, so the return value of the Symbol function with the same parameter is not equal.

// let s1 = Symbol(); let s2 = Symbol(); S1 === s2 // false // let s1 = ('foo'); let s2 = Symbol('foo'); s1 === s2 // falseCopy the code

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

The Symbol value cannot be evaluated with other types of values and will report 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
Copy the code

However, the Symbol value can be explicitly converted to a string.

let sym = Symbol('My symbol');

String(sym) // 'Symbol(My symbol)'
sym.toString() // 'Symbol(My symbol)'
Copy the code

In addition, the Symbol value can be converted to a Boolean value, but not to a value.

let sym = Symbol(); Boolean(sym) // true ! sym // false if (sym) { // ... } Number(sym) // TypeError sym + 2 // TypeErrorCopy the code

Symbol.prototype.description

When creating a Symbol, you can add a description.

const sym = Symbol('foo');
Copy the code

In the code above, sym is described as the string foo.

However, reading this description requires converting Symbol explicitly to a string, as follows.

const sym = Symbol('foo');

String(sym) // "Symbol(foo)"
sym.toString() // "Symbol(foo)"
Copy the code

The above usage is not very convenient. ES2019 provides an instance attribute, Description, that returns the description of Symbol directly.

const sym = Symbol('foo');

sym.description // "foo"
Copy the code

Symbol as the attribute name

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 useful in cases where an object is made up of multiple modules, preventing a key from being accidentally overwritten or overwritten.

let mySymbol = Symbol(); // let a = {}; a[mySymbol] = 'Hello! '; // let a = {mySymbol]: 'Hello! '}; // let a = {}; Object.defineProperty(a, mySymbol, { value: 'Hello! '}); A [mySymbol] // "Hello!"Copy the code

The above code specifies the attribute name of the Object as a Symbol value using square brackets and Object.defineProperty.

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

const mySymbol = Symbol(); const a = {}; a.mySymbol = 'Hello! '; a[mySymbol] // undefined a['mySymbol'] // "Hello!"Copy the code

In the above code, because the dot operator is always followed by a string, the value mySymbol refers to as the identifier name is not read, resulting in the attribute name of A being actually a string rather than a Symbol value.

Similarly, inside an object, when defining an attribute using the Symbol value, the Symbol value must be placed in square brackets.

let s = Symbol(); let obj = { [s]: function (arg) { ... }}; obj[s](123);Copy the code

In the above code, if s is not enclosed in square brackets, the property’s key name is the string S, not the Symbol value that s represents.

With enhanced object writing, the obJ object of the above code can be written more succinctly.

let obj = { [s](arg) { ... }};Copy the code

The Symbol type can also be used to define a set of constants that are guaranteed to have unequal values.

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');
Copy the code

Here’s 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'); }}Copy the code

The great advantage of using the Symbol value for constants is that no other value can have the same value, thus ensuring that the switch statement above works as designed.

It is also important to note that when the Symbol value is used as the property name, the property is still public, not private.

Example: Eliminate magic strings

A magic string is a specific string or number that occurs multiple times in the code and is strongly coupled to the code. Well-styled code should eliminate magic strings as much as possible and replace them with well-defined variables.

function getArea(shape, options) { let area = 0; Switch (shape) {case 'Triangle': // area =.5 * options.width * options.height; break; / *... more code ... */ } return area; } getArea('Triangle', { width: 100, height: 100 }); // Magic stringCopy the code

In the code above, the string Triangle is a magic string. It occurs multiple times, creating a “strong coupling” with the code that is not conducive to future modifications and maintenance.

A common way to eliminate a magic string is to write it as a variable.

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 });
Copy the code

In the code above, we wrote Triangle as the Triangle property of the shapeType object, eliminating strong coupling.

If you look closely, you can see that it doesn’t matter which value shapeType.triangle equals, just make sure it doesn’t conflict with the values of other shapeType attributes. Therefore, this is a good place to use the Symbol value.

const shapeType = {
  triangle: Symbol()
};
Copy the code

In the above code, nothing needs to be changed except to set the shapetype. triangle value to a Symbol.

Traversal of attribute names

Symbol is the attribute name. When traversing the object, the attribute does not appear in the for… In, for… Of loop, will not be the Object. The keys (), Object, getOwnPropertyNames (), JSON. The stringify () returns.

However, it is not private property, with an Object. The getOwnPropertySymbols () method, which can get all the Symbol of specified Object attribute names. This method returns an array of all the Symbol values used as attribute names for 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)]
Copy the code

Code above is Object. GetOwnPropertySymbols () method of sample, you can get all the Symbol attribute names.

Here is another example of the Object. GetOwnPropertySymbols () method with the for… In circulation, the Object. Comparing getOwnPropertyNames method example.

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)]Copy the code

In the code above, use for… In circulation and the Object. GetOwnPropertyNames () methods are not Symbol key name, you need to use the Object. The getOwnPropertySymbols () method.

Another new API, the reflecti.ownkeys () method, returns all types of key names, including regular and Symbol.

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

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

Because the Symbol value is used as the key name, it is not iterated by the normal method. We can take advantage of this feature to define non-private methods for objects that we want to use only internally.

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)]Copy the code

The above code, the Object the size attribute is a Symbol of x value, so the Object. The keys (x), Object. GetOwnPropertyNames (x) can get it. This creates the effect of a non-private internal approach.

Symbol. The for (), Symbol. KeyFor ()

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

let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');

s1 === s2 // true
Copy the code

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

Symbol. For () and Symbol() both generate a new Symbol. The difference is that the former will be registered in the global environment for search, while the latter will not. Symbol.for() does not return a new Symbol value each time it is called. Instead, it checks to see if the given key already exists and creates a new value if it does not. For example, if you call symbol. for(“cat”)30 times, the same Symbol will be returned each time, but calling Symbol(“cat”)30 times will return 30 different symbols.

Symbol.for("bar") === Symbol.for("bar")
// true

Symbol("bar") === Symbol("bar")
// false
Copy the code

In the code above, since the Symbol() notation has no registration mechanism, each call returns a different value.

The symbol.keyfor () method returns the key of a registered Symbol type value.

let s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"

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

In the code above, the variable s2 belongs to an unregistered Symbol value, so undefined is returned.

Note that symbol.for () registers the name of the Symbol value for the global environment, whether or not it is running in the global environment.

function foo() {
  return Symbol.for('bar');
}

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

In the code above, symbol.for (‘bar’) is run inside the function, but the generated Symbol value is registered globally. So, a second run of symbol. for(‘bar’) fetches the Symbol value.

The global registration feature of symbol.for () can be used to fetch the same value from 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
Copy the code

In the code above, the Symbol value generated by the iframe window is available on the main page.

Example: Module’s Singleton pattern

The Singleton pattern refers to invoking a class that returns the same instance at all times.

For Node, a module file can be thought of as a class. How do I guarantee that the same instance will be returned each time I execute this module file?

It’s easy to imagine putting an instance into the top-level object, Global.

// mod.js function A() { this.foo = 'hello'; } if (! global._foo) { global._foo = new A(); } module.exports = global._foo;Copy the code

Then, load the mod.js above.

const a = require('./mod.js');
console.log(a.foo);
Copy the code

In the code above, variable A is always loaded with the same instance of A.

There is a problem, however, that the global variable global._foo is writable and can be modified by any file.

global._foo = { foo: 'world' };

const a = require('./mod.js');
console.log(a.foo);
Copy the code

The above code distorts any script that loads mod.js.

To prevent this from happening, 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];Copy the code

In the code above, we can ensure that global[FOO_KEY] is not inadvertently overwritten, but it can still be overwritten.

global[Symbol.for('foo')] = { foo: 'world' };

const a = require('./mod.js');
Copy the code

If the key name is generated using the Symbol method, the value cannot be referenced externally and therefore cannot be overwritten.

// mod.js const FOO_KEY = Symbol('foo'); // The following code is the same...Copy the code

The above code will cause no other script to reference FOO_KEY. The problem with this is that if you execute the script multiple times, you’ll get a different FOO_KEY each time. Although Node caches the results of script execution, the script is not normally executed more than once, but the cache can be cleared manually by the user, so it is not foolproof.

The built-in Symbol value

In addition to defining the Symbol values you use, ES6 provides 11 built-in Symbol values that point to methods used within the language.

Symbol.hasInstance

Object that points to an internal method. This method is called when another object uses the instanceof operator to determine whether it is an instanceof that object. For example, foo instanceof foo inside the language actually calls foo [symbol.hasinstance](foo).

class MyClass {
  [Symbol.hasInstance](foo) {
    return foo instanceof Array;
  }
}

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

In the above code, MyClass is a class, and new MyClass() returns an instance. The symbol.hasinstance method on this instance is called automatically during instanceof to determine whether the operator on the left is an instanceof Array.

Here’s another example.

class Even { static [Symbol.hasInstance](obj) { return Number(obj) % 2 === 0; }} // const Even = {[symbol.hasinstance](obj) {return Number(obj) % 2 === 0; }}; 1 instanceof Even // false 2 instanceof Even // true 12345 instanceof Even // falseCopy the code

Symbol.isConcatSpreadable

Object Symbol. IsConcatSpreadable attribute is a Boolean value, said that the object is used to Array. The prototype. The concat (), whether can be expanded.

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']
Copy the code

Code above, the default behavior of the array is can expand, Symbol. IsConcatSpreadable default is undefined. This property is also expanded when it is true.

Array-like objects, by contrast, are not expanded by default. Its Symbol. IsConcatSpreadable attribute set to true, can be carried out.

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']
Copy the code

Symbol. IsConcatSpreadable properties can also be defined inside the 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]]
Copy the code

In the code above, class A1 is expandable and class A2 is unexpandable, so using concat gives different results.

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

Symbol.species

Object’s symbol. species property, pointing to a constructor. This property is used when a derived object is created.

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
Copy the code

In the code above, subclass MyArray inherits its parent class Array, with a being an instance of MyArray and B and C being the derived objects of A. You might think that b and C would be arrays (instances of Array) because they are both generated by calling Array methods, but they are also instances of MyArray.

The symbol.species attribute 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; }}Copy the code

In the code above, since the symbol. species attribute is defined, the derived object is created using the function returned by this attribute as its constructor. This example also shows that the get valuer is used to define the symbol. species attribute. The default symbol. species attribute is equivalent to the following.

static get [Symbol.species]() {
  return this;
}
Copy the code

Now, look at 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
Copy the code

In the code above, the derived object generated by a.map(x => x) is not an instance of MyArray, but an instance of Array.

Let’s do 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
Copy the code

In the code above, T2 defines the symbol. species attribute, T1 does not. The result is that when a derived object is created (then methods), T1 calls its own constructor, while T2 calls Promise’s constructor.

In summary, the use of symbol.species is that the constructor specified by this property is called when the instance object needs to call its own constructor again during runtime. Its main use is that some libraries are modified from the base class, so when a subclass uses an inherited method, the author may want to return an instance of the base class instead of the subclass.

Symbol.match

Object that points to a function. When str.match(myObject) is executed, if the property exists, it is called, returning the return value of the method.

Prototype match(regexp) // equivalent to regexp[symbol.match](this) class MyMatcher {[symbol.match](String) {return 'hello world'.indexOf(string); } } 'e'.match(new MyMatcher()) // 1Copy the code

Symbol.replace

The Symbol. Replace property of the string.prototype. replace method that points to a method whose return value is returned when the object is called by the String.prototype.replace method.

String.prototype.replace(searchValue, replaceValue) // Equivalent to searchValue[symbol.replace](this, replaceValue)Copy the code

Here’s an example.

const x = {}; x[Symbol.replace] = (... s) => console.log(s); 'Hello'.replace(x, 'World') // ["Hello", "World"]Copy the code

The symbol.replace method receives two arguments. The first argument is the object on which the replace method is working (Hello, in this example), and the second argument is the replaced value (World, in this example).

Symbol.search

Object that points to a method whose return value is returned when called by the String.prototype.search method.

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')) // 0Copy the code

Symbol.split

Object’s symbol. split property, which points to a method whose return value is returned when the object is called by the String.prototype.split method.

String. Prototype. Split (separator, limit) // Equal to separator[symbol.split](this, limit)Copy the code

Here’s 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'
Copy the code

The above method uses the symbol.split method to redefine the behavior of the split method on string objects,

Symbol.iterator

Object that points to the default traverser method for that object.

const myIterable = {};
myIterable[Symbol.iterator] = function* () {
  yield 1;
  yield 2;
  yield 3;
};

[...myIterable] // [1, 2, 3]
Copy the code

Object for… Iterator returns the default iterator for the object, described in the chapter Iterator and for…of loops.

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 // 2Copy the code

Symbol.toPrimitive

Object’s symbol. toPrimitive property, pointing to a method. This method is called when the object is converted to a value of the original type, returning the corresponding value of the original type of the object.

When symbol.toprimitive is called, it takes a string argument representing the current operation mode, of which there are three.

  • Number: Converts to a value in this case

  • String: Converts to a String in this case

  • Default: The value can be converted to a value or a string

    let obj = { Symbol.toPrimitive { 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’

Symbol.toStringTag

Object that points to a method. Call on the Object Object. The prototype. The toString method, if the property exists, it returns a value will appear in the toString () method returns the string, the type of the Object. That is, this property can be used to customize the string after object in object object or object Array.

/ / a ({[Symbol toStringTag] : 'Foo'}.toString()) // "[object Foo]" // example 2 Class Collection {get [symbol.toString]() {return 'XXX '; } } let x = new Collection(); Object.prototype.toString.call(x) // "[object xxx]"Copy the code

The Symbol. ToStringTag property values for ES6’s new built-in objects are shown below.

  • JSON[Symbol.toStringTag]: ‘JSON’
  • Math[Symbol.toStringTag]: ‘Math’
  • The Module objectM[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 the Iterator’
  • %StringIteratorPrototype%[Symbol.toStringTag]: ‘String Iterator’
  • Symbol.prototype[Symbol.toStringTag]: ‘Symbol’
  • Generator.prototype[Symbol.toStringTag]: ‘Generator’
  • GeneratorFunction.prototype[Symbol.toStringTag]: ‘GeneratorFunction’

Symbol.unscopables

Object’s symbol. unscopables property pointing to an object. This object specifies which attributes 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']
Copy the code

As the code above shows, the array has seven attributes that are excluded by the with command.

// class MyClass {foo() {return 1; } } var foo = function () { return 2; }; with (MyClass.prototype) { foo(); // class MyClass {foo() {return 1; } get [Symbol.unscopables]() { return { foo: true }; } } var foo = function () { return 2; }; with (MyClass.prototype) { foo(); / / 2}Copy the code

The code above makes the with block not look for foo in the current scope by specifying the symbol.unscopables property, meaning that foo will refer to variables in the outer scope.