Symbol type in ES6

Posted by jxrd on Sun, 09 Jun 2019 22:57:07 +0200

Previous remarks

ES5 contains five primitive types: Character string,number,Boolean value,null and undefined . ES6 introduces the sixth primitive type, Symbol

The object attribute names of ES5 are strings, which can easily cause property name conflicts. For example, if you use an object provided by someone else and want to add a new method to the object, the name of the new method may conflict with the existing method. If there is a mechanism to ensure that each attribute's name is unique, it fundamentally prevents attribute name conflicts. This is why ES6 introduced Symbol. This article will give a detailed description of the Symbol type in ES6.

 

Establish

Symbol values are generated by Symbol functions. That is to say, there are two types of attribute names for an object: a string and a Symbol type. Any attribute name that belongs to the Symbol type is unique and guarantees that it does not conflict with other attribute names.

let firstName = Symbol();
let person = {};
person[firstName] = "huochai";
console.log(person[firstName]); // "huochai"

[Note] The new command cannot be used before the Symbol function, otherwise an error will be reported. Because the generated Symbol is a primitive type of value, not an object

//Uncaught TypeError: Symbol is not a constructor
let firstName = new Symbol();

The Symbol function accepts an optional parameter that adds a text to describe the Symbol to be created. This description is not available for attribute access, but it is recommended that such a description be added every time a Symbol is created to facilitate reading code and debugging the Symbol program.

let firstName = Symbol("first name");
let person = {};
person[firstName] = "huochai";
console.log("first name" in person); // false
console.log(person[firstName]); // "huochai"
console.log(firstName); // "Symbol(first name)"

Symbol's description is stored in the internal [[Description]] property, which can be read only when Symbol's toString() method is called. The toString() method of firstName is implicitly called when console.log() is executed, so its description is printed to the log, but it cannot be accessed directly in code [[Description]]

[Type Detection]

Symbol is the original value, and ES6 extends the typeof operator to return "symbol". So you can use typeof to detect whether the variable is symbolic

let symbol = Symbol("test symbol");
console.log(typeof symbol); // "symbol"

 

Use

Since each Symbol value is not identical, this means that the Symbol value can be used as an identifier for the object's attribute name, which ensures that no attribute with the same name will appear. This is very useful when an object is made up of multiple modules and prevents a key from being accidentally overwritten or overwritten.

Symbol is available for all places where computable attribute names are used

let firstName = Symbol("first name");
// Use a literal property to compute
let person = {
    [firstName]: "huochai"
};
// Make this property read-only
Object.defineProperty(person, firstName, { writable: false });
let lastName = Symbol("last name");
Object.defineProperties(person, {
    [lastName]: {
        value: "match",
        writable: false
    }
});
console.log(person[firstName]); // "huochai"
console.log(person[lastName]); // "match"

In this example, we first create a Symbol attribute firstName for the person object through the computable object literal attribute grammar. The next line of code sets this property to read-only. Subsequently, a read-only Symbol attribute lastName is created through the Object. defineProperties () method, where the object literal property is used again, but it is used as the second parameter of the object. defineProperties () method.

[Note] When the Symbol value is used as the object attribute name, the point operator cannot be used

var mySymbol = Symbol();
var a = {};

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

As can be seen from the above results, mySymbol in a.mySymbol and a['mySymbol'] is the attribute name of string type, and mySymbol in a[mySymbol] is the attribute name of Symbol type. Although they are all called mySymbol, the values are different.

Although Symbol can be used in all places where computable attribute names are used, in order to share these symbols effectively among different code fragments, a system needs to be established.

 

Sharing System

Sometimes you want to share the same Symbol in different code. For example, there are two different object types in the application, but you want them to use the same Symbol attribute to represent a unique identifier. Generally speaking, tracking Symbol in a large code base or across files is very difficult and error-prone. For these reasons, ES6 provides a global Symbol registry that can be accessed at any time.

[Symbol.for()]

If you want to create a shared Symbol, use the Symbol.for() method. It accepts only one parameter, the string identifier of the Symbol to be created, which is also used as a description of Symbol.

let uid = Symbol.for("uid");
let object = {};
object[uid] = "12345";
console.log(object[uid]); // "12345"
console.log(uid); // "Symbol(uid)"

The Symbol.for() method first searches for the existence of Symbol with the key "uid" in the global Symbol registry. If it exists, return the existing Symbol directly. Otherwise, create a new Symbol and use this key to register in the Symbol global registry, and then return the newly created Symbol.

Subsequently, if the same key is passed in to call Symbol.for(), the same Symbol will be returned.

let uid = Symbol.for("uid");
let object = {
    [uid]: "12345"
};
console.log(object[uid]); // "12345"
console.log(uid); // "Symbol(uid)"
let uid2 = Symbol.for("uid");
console.log(uid === uid2); // true
console.log(object[uid2]); // "12345"
console.log(uid2); // "Symbol(uid)

In this example, uid and uid2 contain the same Symbol and can be used interchangeably. The first call to the Symbol.for() method creates the Symbol, and the second call retrieves the Symbol directly from the Symbol global registry.

[Symbol.keyFor()]

There is another feature associated with Symbol sharing: Symbol-related keys can be retrieved in the Symbol global registry using the Symbol.keyFor() method

let uid = Symbol.for("uid");
console.log(Symbol.keyFor(uid)); // "uid"
let uid2 = Symbol.for("uid");
console.log(Symbol.keyFor(uid2)); // "uid"
let uid3 = Symbol("uid");
console.log(Symbol.keyFor(uid3)); // undefined

Both uid and uid2 return the "uid" key, but in the Symbol Global Registry there is no uid3 Symbol, that is, there is no key associated with it, so ultimately undefined is returned.

[Note] The name of Symbol.for registered for the Symbol value is global and can be retrieved from a different iframe or service worker.

let iframe = document.createElement('iframe');
iframe.src = String(window.location);
document.body.appendChild(iframe);

console.log(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.

Symbol global registry is a shared environment similar to global scope, that is to say, it cannot assume which keys exist in the current environment. When using third-party components, try to use the Symbol key namespace to reduce naming conflicts. For example, jQuery's code can prefix "jquery" to all keys, like "jquery.element" or other similar keys.

 

Type Conversion

Type conversion is an important language feature in JS, but other types do not have the same value as Symbol logic, so Symbol is not very flexible to use.

The console.log() method is used to output Symbol content, which calls Symbol's String () method and outputs useful information. You can also call the string () method directly like this to get the same content.

let uid = Symbol.for("uid"),
    desc = String(uid);
console.log(desc); // "Symbol(uid)"

The String() function calls the uid.toString() method and returns the contents of the Symbol description of the string type. However, if you try to splice Symbol with a string, the program will throw an error.

let uid = Symbol.for("uid"),
desc = uid + ""; // Cause a mistake!

When splicing uid with empty strings, the first step is to force uid to be converted to a string, while Symbol cannot be converted to a string, so the program throws an error directly.

Similarly, Symbol cannot be coerced into a digital type. Mixing Symbol with every mathematical operator causes the program to throw errors

let uid = Symbol.for("uid"),
sum = uid / 1; // Cause a mistake!

Trying to divide Symbol by 1, the program throws an error directly. And no matter which mathematical operator is used, it will not work properly.

[Note] Except for Boolean values, because Symbol is similar to non-null values in JS, and its equivalent Boolean value is true

let uid = Symbol.for("uid");
console.log(uid);//'Symbol(uid)'
console.log(!uid);//false
console.log(Boolean(uid));//true

 

Attribute Retrieval

Symbol, as the attribute name, will not appear in the for...in, for...of loop, nor will it be returned by Object. getOwnProperty Names (), Object.keys(), JSON.stringify(). Thus, an Object.getOwnpropertySymbols() method was added to ES6 to retrieve the Symbol attribute in the object.

The return value of the Object.getOwnPropertySymbols() method is an array containing all Symbol's own properties

let uid = Symbol.for("uid");
let object = {
    [uid]: "12345"
};
let symbols = Object.getOwnPropertySymbols(object);
console.log(symbols.length); // 1
console.log(symbols[0]); // "Symbol(uid)"
console.log(object[symbols[0]]); // "12345"

In this code, the object object object object has a Symbol property called uid, and the object.getOwnPropertySymbols() method returns an array containing this property

Another new API -- Reflect.ownKeys() method returns all types of key names, including regular and Symbol key names

let obj = {
  [Symbol('my_key')]: 1,
  enum: 2,
  nonEnum: 3
};
console.log(Reflect.ownKeys(obj));//  ["enum", "nonEnum", Symbol(my_key)]

Because of the properties with Symbol values as names, they will not be traversed by conventional methods. This feature can be used to define some non-private, but hopefully only internal methods for objects.

var size = Symbol('size');

class Collection {
  constructor() {
    this[size] = 0;
  }

  add(item) {
    this[this[size]] = item;
    this[size]++;
  }

  static sizeOf(instance) {
    return instance[size];
  }
}

var 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 code above, the size attribute of 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 approach.

 

Built-in Symbol

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

  1,Symbol.haslnstance

An internal method called when instanceof is executed to detect inheritance information of an object

  2,Symbol.isConcatSpreadable

A Boolean value that indicates whether elements in a collection should be regularized to the same level when passing a collection as a parameter to the Array.prototype.concat() method

  3,Symbol.iterator

A method of returning iterators

  4,Symbol.match

A method called when calling the String.prototype.match() method to compare strings

  5,Symbol.replace

A method called when calling the String.prototype.replace() method to replace substrings of strings

  6,Symbol.search

A method called when calling the String.prototype.search() method to locate substrings in strings

  7,Symbol.species

Constructor for creating derived classes

  8,Symbol.split

A method called when calling the String.prototype.split() method to split strings

  9,Symbol.toprimitive

A method of returning the original value of an object

  10,Symbol.ToStringTag

A string used to create an object description when calling the Object.prototype.toString() method

  11,Symbol.unscopables

An object set that defines object attribute names that cannot be referenced by with statements

[Symbol.haslnstance]

Each function has a Symbol.haslnstance method to determine whether an object is an instance of a function. This method is defined in Function.prototype, and all functions inherit the default behavior of instanceof attributes. To ensure that Symbol. haslnstance is not accidentally rewritten, this method is defined as not writable, configurable, and enumerable.

The Symbol.haslnstance method accepts only one parameter, the value to be checked. If the incoming value is an instance of a function, return true

obj instanceof Array;

The above line of code is equivalent to the following line

Array[Symbol.hasInstance](obj);

Essentially, ES6 simply redefines the instanceof operator as a shorthand grammar for this method. Now that method calls are introduced, you can change the way instanceof works at will.

class MyClass {
  [Symbol.hasInstance](foo) {
    return foo instanceof Array;
  }
}
console.log([1, 2, 3] instanceof new MyClass()); // true

Assuming that an instance-free function is defined, the return value of Symbol.haslnstance can be hard-coded as false

function MyObject() {
    // ...
}
Object.defineProperty(MyObject, Symbol.hasInstance, {
    value: function(v) {
        return false;
    }
});
let obj = new MyObject();
console.log(obj instanceof MyObject); // false

The above example calls this method to rewrite symbol.haslnstance, defining a new function that always returns false, even if obj is actually an instance of Myobject class, the instanceof operator returns false after calling the object. defineProperty () method.

Of course, based on arbitrary conditions, value checking can also be used to determine whether the detected case is an instance. For example, you can define a number from 1 to 100 as an example of a special number type, with the following code for implementation

function SpecialNumber() {
  // empty
}
Object.defineProperty(SpecialNumber, Symbol.hasInstance, {
    value: function(v) {
        return (v instanceof Number) && (v >=1 && v <= 100);
    }
});
let two = new Number(2),
zero = new Number(0);
console.log(two instanceof SpecialNumber); // true
console.log(zero instanceof SpecialNumber); // false

In this code, a symbol.hasInstance method is defined, which returns true when the value is an instance of Number and its value is between 1 and 100. So even if there is no direct relationship between the SpecialNumber function and the variable two, the variable two is recognized as an instance of the special Number.

If the Symbol.haslnstance call is to be triggered, the left operand of instanceof must be an object, and if the left operand is non-object, instanceof always returns false.

Of course, you can override the default symbol. haslnstance attribute for all built-in functions, such as Date and Error functions. However, the consequence of this is that the result of the code running becomes unpredictable and may be confusing, so it is not recommended to do so. The best way to do this is to rewrite the Symbol. haslnstance attribute of the function you declare only if necessary.

[Symbol.isConcatSpreadable]

The Symbol.isConcatSpreadable property of the object is a Boolean value indicating whether the object can be expanded when it uses 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']

The code above shows that the default behavior of an array can be expanded. The Symbol.isConcatSpreadable attribute equals undefined or true, and both have this effect.

The class array object can also be expanded, but its Symbol.isConcatSpreadable property defaults to false and must be opened manually.

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']

For a class, the Symbol.isConcatSpreadable property must be written as an instance property

class A1 extends Array {
  constructor(args) {
    super(args);
    this[Symbol.isConcatSpreadable] = true;
  }
}
class A2 extends Array {
  constructor(args) {
    super(args);
    this[Symbol.isConcatSpreadable] = 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, class A1 is expandable, class A2 is not expandable, so using concat has different results

[Symbol.species]

The Symbol.species property of the object points to the constructor of the current object. When creating an instance, this method is called by default, even if the function returned by this attribute is used as a constructor to create a new instance object.

class MyArray extends Array {
  // Override the parent class Array Constructive function
  static get [Symbol.species]() { return Array; }
}

In the code above, the subclass MyArray inherits the parent class Array. When creating an instance object of MyArray, it would have called its own constructor, but since the Symbol.species attribute is defined, it will use the function returned by this attribute to create an instance of MyArray.

This example also shows that the get reader is used to define the Symbol.species attribute. The default Symbol.species attribute is equivalent to the following

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

Here's an example

class MyArray extends Array {
  static get [Symbol.species]() { return Array; }
}
var a = new MyArray(1,2,3);
var mapped = a.map(x => x * x);

mapped instanceof MyArray // false
mapped instanceof Array // true

In the above code, because the constructor was replaced with Array. So, the mapped object is not an instance of MyArray, but an instance of Array.

[Symbol.match]

The Symbol.match attribute of the object points to a function. When str.match(myObject) is executed, if the attribute exists, it is called and the return value of the method is 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

[Symbol.replace]

The Symbol.replace property of the object, which points to a method, returns the return value of the String.prototype.replace method when the object is called by the String.prototype.replace method

String.prototype.replace(searchValue, replaceValue)
// Equivalent to
searchValue[Symbol.replace](this, replaceValue)

Here's an example

const x = {};
x[Symbol.replace] = (...s) => console.log(s);

'Hello'.replace(x, 'World') // ["Hello", "World"]

The Symbol.replace method receives two parameters. The first parameter is the object in which the replace method is working. The example above is Hello. The second parameter is the value after the replacement. The example above is World.

[Symbol.search]

The Symbol.search attribute of the object, which points to a method, returns the return value of the String.prototype.search method when the object is called by the String.prototype.search method

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

[Symbol.split]

The Symbol.split attribute of the object, which points to a method, returns the return value of the String.prototype.split method when the object is called by the String.prototype.split method

String.prototype.split(separator, limit)
// Equivalent to
separator[Symbol.split](this, limit)

Here's an example

class MySplitter {
  constructor(value) {
    this.value = value;
  }
  [Symbol.split](string) {
    var 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'

Using the Symbol.split method, the above method redefines the behavior of the split method for string objects.

[Symbol.iterator]

Object's Symbol.iterator property, pointing to the default traversal method of the object

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

[...myIterable] // [1, 2, 3]

The Symbol.iterator method is called to return the default traversal of the object when the object is looped for...of.

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

[Symbol.toPrimitive]

The Symbol.toPrimitive property of the object points to a method. When the object is converted to the value of the original type, this method is called to return the value of the original type corresponding to the object.

When Symbol.toPrimitive is called, it accepts a string parameter representing the current mode of operation. There are three modes.

Number: Conversion to numerical values is required in this case

2. String: This occasion needs to be converted to a string

Default: In this case, it can be converted to a numerical value or to 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'

[String.toStringTag]

The Symbol.toStringTag property of the object points to a method. When the Object.prototype.toString method is called on the object, if this property exists, its return value will appear in the string returned by the toString method, representing the type of the object. That is to say, this property can be used to customize the string after the object in [object Object object] or [object Array].

// Example 1
({[Symbol.toStringTag]: 'Foo'}.toString())
// "[object Foo]"

// Example two
class Collection {
  get [Symbol.toStringTag]() {
    return 'xxx';
  }
}
var x = new Collection();
Object.prototype.toString.call(x) // "[object xxx]"

The Symbol.toStringTag attribute values of the new built-in object in ES6 are as follows:

    JSON[Symbol.toStringTag]: 'JSON'
    Math[Symbol.toStringTag]: 'Math'
    Module[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'
    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]

The Symbol.unscopables attribute of the object points to an object. This object specifies which attributes will be excluded by the with environment when using the with keyword.

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']

The above code shows that the array has seven attributes and will be excluded by the with command.

// No unscopables time
class MyClass {
  foo() { return 1; }
}

var foo = function () { return 2; };

with (MyClass.prototype) {
  foo(); // 1
}

// Yes unscopables time
class MyClass {
  foo() { return 1; }
  get [Symbol.unscopables]() {
    return { foo: true };
  }
}

var foo = function () { return 2; };

with (MyClass.prototype) {
  foo(); // 2
}

By specifying the Symbol.unscopables attribute, the above code prevents the with grammar block from looking for foo attributes in the current scope, which means that foo will point to variables in the outer scope.

Topics: Javascript Attribute JQuery JSON