I’m about to write a couple of posts that involve the use of Symbols in JavaScript so thought I better do a quick primer that I can link to for people who aren’t familiar with them.

What are Symbols

A Symbol is a JavaScript primitive (like a String, Boolean, or Array etc). It is essentially a GUID, guaranteed to be unique but with no string representation. Well okay technically it does have a string representation which you can access with .toString() however this will always output Symbol() rather than exposing whatever unique value has been generated by Symbol().

function test()  {
  // Note that you should not use the 'new' keyword
  const guid = Symbol()

  const obj = {
    [guid]: '123456',
    name: 'Bob',
    age: 42,
  }

  console.log(obj)
  // { name: 'Bob', age: 42, [Symbol()]: '123456' }
}

test();

In the above example we create a Symbol and assign it’s value to a variable called guid, we can then use this variable to reference the Symbol in our code. Here we use it as a property name in an object literal, when we log to the console you can see that the object has a property that is referenced by a Symbol and has the value 123456.

If we want to access the value of the GUID directly we can do so using the standard bracket notation:

function test()  {
  const guid = Symbol()

  const obj = {
    [guid]: '123456',
    name: 'Bob',
    age: 42,
  }

  console.log(obj[guid])
  // 123456
}

test();

Note that because symbols are unique values we can do the following without them clashing:

function addAnotherSymbol(obj) {
  const guid = Symbol()
  obj[guid] = '654321'
  return obj;
}

function test()  {
  const guid = Symbol()

  const obj = {
    [guid]: '123456',
    name: 'Bob',
    age: 42,
  }

  addAnotherSymbol(obj)

  console.log(obj)
  // { name: 'Bob', age: 42, [Symbol()]: '123456', [Symbol()]: '654321' }
}

test();

In both functions we have used a variable called guid however the Symbols that are stored in them are different, every time you call Symbol() it’s a unique value that’s returned, that’s why, if you look at the console output, you will see 2 Symbols() each with different values.

Things You Should Know

Firstly Symbols are ignored by most inbuilt JavaScript functions and methods. For example if you want to iterate over an object using Object.keys(), Object.values(), or Object.entries() they will all ignore your Symbols:

function test()  {
  const guid = Symbol()

  const obj = {
    [guid]: '123456',
    name: 'Bob',
    age: 42,
  }

  const keys = Object.keys(obj)
  for (const key of keys) {
    console.log(key)
    // name
    // age
  }

  const values = Object.values(obj)
  for (const value of values) {
    console.log(value)
    // Bob
    // 42
  }

  const entries = Object.entries(obj)
  for (const entry of entries) {
    console.log(entry)
    // [ 'name', 'Bob' ]
    // [ 'age', 42 ]
  }
}

test();

However this does not mean Symbols are entirely hidden, they are accessible to those who know how:

function test()  {
  const guid = Symbol()

  const obj = {
    [guid]: '123456',
    name: 'Bob',
    age: 42,
  }

  const keys = Reflect.ownKeys(obj)
  for (const key of keys) {
    console.log(obj[key])
    // Bob
    // 42
    // 123456
  }

  const symbols = Object.getOwnPropertySymbols(obj)
  for (const symbol of symbols) {
    console.log(obj[symbol])
    // 123456
  }
}

test();

Secondly, JavaScript uses built in “Well-known” Symbols to implement some of the JavaScript language features. I’m not going to go into details here but if you want to make Objects iterable using a for(const obj of objects) or if you want to customise the behaviour of object.replace('test', '') then Well-known Symbols are how you would do it.

MDN has a handy list of the Well-known Symbols which can be found here.

Here is an example of how you could use toPrimitive to change the return value of an object based on the implicit type that is being requested:

function test()  {
  const user = {
    name: 'Bob',
    age: 42,
  }

  user[Symbol.toPrimitive] = function(hint) {
    if (hint === 'string') {
      return this.name
    } else if (hint === 'number') {
      return this.age
    }
    return 'default'
  }

  console.log(`Name: ${user}`) // implicit conversion to string
  // Name: Bob

  console.log('Age / 10:', user / 10) // implicit conversion to number
  // Age / 10: 4.2

  console.log('Unsure:', user + 10) // Could be arithmetic or concatenation
  // Unsure: default10
}

test();

And here is an example of how we could alter the .toString() conversion of an Object:

function test()  {
  const user = {
    name: 'Bob',
    age: 42,
  }

  console.log(user.toString())
  // [object Object]

  user[Symbol.toStringTag] = 'User'

  console.log(user.toString())
  // [object User]
}

test();

Summary

Symbols are unique values with no unique string representation, they can be used as object property names to prevent naming clashes or hide information from methods such as Object.keys(), Object.values, or Object.entities(). They are not entirely hidden and can be accessed without an existing reference to the Symbol if you know how. So called “Well-known” Symbols are used to customise in built JavaScript language functionality.

As interesting as all this is, if you’re struggling to see where you would use Symbols you’re not alone, in fact I’m probably going to do a follow up post on that very topic. The main thing I can see that’s useful about Symbols is the Well-known Symbols however I wouldn’t recommend making heavy use of these so even that is a bit of an edge case.


0 Comments

Leave a Reply

Avatar placeholder

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.