DEV Community

Cover image for Symbols in JS For Beginners 👨‍💻👩‍💻 With Examples And Exercises
Meat Boy
Meat Boy

Posted on

Symbols in JS For Beginners 👨‍💻👩‍💻 With Examples And Exercises

In this post, I am going to explain what is a Symbol in JavaScript, when and how to use it. At the end of the post are a few exercises. You can check yourself and post solutions in the comment. First few answers I'll code review 😉

So, let's learn something new!
Learning

What is a Symbol?

The Symbol is a new primitive data type, introduced with ECMAScript 6. Every symbol created with basic constructor is unique.

const symbol1 = Symbol(); // create first symbol
const symbol2 = Symbol(); // create second symbol

console.log(symbol1 == symbol2); // false
console.log(symbol1 === symbol2); // false
Enter fullscreen mode Exit fullscreen mode

Symbol can be created with description in the constructor. However, it shouldn't be used for any other purpose than debuging. Don't relay on the description!

const niceSymbol = Symbol('Yup 👩‍💻');
console.log(niceSymbol.description); // Yup 👩‍💻 
Enter fullscreen mode Exit fullscreen mode

Global symbol registry

The symbol can be also created from method for with custom string as the argument. So you can create few instances of symbol with the same value under the hood. After creating symbol by method for, the description is set to the same value as key and the symbol itself is store in global symbol registry.

const symbol1 = Symbol.for('devto');
const symbol2 = Symbol.for('devto');

console.log(symbol1 == symbol2); // true
console.log(symbol1 === symbol2); // true
console.log(symbol1.description); // devto
Enter fullscreen mode Exit fullscreen mode

Global symbol registry is a location where all symbols created with for method are store across all contexts in the runtime. When you are using for method for the first time, new symbol is attached to the registry. Next time is retrieving from it.

What important, symbols created with for method are distinct from those created with the basic constructor. You can check key for symbol registered globally with method Symbol.keyFor().

const a = Symbol.for('devto'); // globally registered symbol
console.log(Symbol.keyFor(a)); // devto

const b = Symbol(); // local unique symbol
console.log(Symbol.keyFor(b)); // undefined
Enter fullscreen mode Exit fullscreen mode

Symbols don't have string literals. So if you try to explicitly convert a symbol to a string, you get TypeError.

console.log(`${Symbol()}`); // TypeError: Can't convert Symbol to string
Enter fullscreen mode Exit fullscreen mode

Hide access to property

Symbols are commonly used for hiding direct access to properties in objects. With Symbol, you can create a semi-private field.

Props are hidden like pink panther ;) They exist, you can retrieve them with some effort but at first glance, you cannot see and cannot get them!
Hide

const tree = {
  [Symbol('species')]: 'birch',
  [Symbol('height')]: 7.34,
};
console.log(tree);
Enter fullscreen mode Exit fullscreen mode

Without reference to a symbol, you don't have value under which properties are bound to tree.

Enum

Another awesome trick to do with symbols is to create Enum. Enums in another programming languages are types with all possible values. For instance, you may want to have exactly two states of car: DRIVE and IDLE and make sure, car state comes from this enum so you can't use string or numbers.

Example of enum with symbols:

const CarState = Object.freeze({
  DRIVE: Symbol('drive'),
  IDLE: Symbol('idle'),
});

const car = {
  state: CarState.DRIVE
}

if (car.state === CarState.DRIVE) {
  console.log('Wroom, wroom 🚙!');
} else if (car.state === CarState.IDLE) {
  console.log('Waiting for ya ⏱!');
} else {
  throw new Error('Invalid state');
}

// Wroom, wroom 🚙!
Enter fullscreen mode Exit fullscreen mode

Why symbols are so important? Check this example. If you try to mutate object with other value than is behind symbol from enum you will get an error.

// correct way of creating enum - with symbols

const CarState = Object.freeze({
  DRIVE: Symbol('drive'),
  IDLE: Symbol('idle'),
});

const car = {
  state: CarState.DRIVE
}

// you cannot set the state without reference to symbol-based enum
car.state = 'idle';

if (car.state === CarState.DRIVE) {
  console.log('Wroom, wroom 🚙!');
} else if (car.state === CarState.IDLE) {
  console.log('Waiting for ya ⏱!');
} else {
  throw new Error('Invalid state');
}

// Error: Invalid state
Enter fullscreen mode Exit fullscreen mode

Similiar code with strings will be valid, and this is a problem! We want to control all possible states.

// invalid way of creating enum - with other data types

const CarState = Object.freeze({
  DRIVE: 'drive',
  IDLE: 'idle',
});

const car = {
  state: CarState.DRIVE
}

// you can set car state without calling for enum prop, so data may be lost or incorrect
car.state = 'idle';

if (car.state === CarState.DRIVE) {
  console.log('Wroom, wroom 🚙!');
} else if (car.state === CarState.IDLE) {
  console.log('Waiting for ya ⏱!');
} else {
  throw new Error('Invalid state');
}
// Waiting for ya ⏱!
Enter fullscreen mode Exit fullscreen mode

Well-known Symbols

The last thing is a set of well-known symbols. They are built-in properties and are used for different internal object behaviours. This is a little tricky topic. So let say we want to override Symbol. iterator, the most popular well-known symbol for objects.

Iterator is responsible for behaviour when we are iterating with for of loop.

const tab = [1, 7, 14, 4];

for (let num of tab) {
  console.log(num);
}
// 1
// 7
// 14
// 4
Enter fullscreen mode Exit fullscreen mode

Roman numeral

But what if we want to return all numbers but in the Roman numeral and without changing for of loop? We can use Symbol.iterator and override function responsible for returning values.

const tab = [1, 7, 14, 4];

tab[Symbol.iterator] = function () {
  let index = 0;
  const total = this.length;
  const values = this;
  return {
    next() {
      const romanize = num => {
        const dec = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1];
        const rom = ["M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"];
        let output = "";

        for (let i = 0; i < dec.length; i++) {
          while (dec[i] <= num) {
            output += rom[i];
            num -= dec[i];
          }
        }

        return output;
      };

      return index++ < total ? {
        done: false,
        value: romanize(values[index - 1])
      } : {
        done: true
      };
    }

  };
};

for (let num of tab) {
  console.log(num);
}
// I
// VII
// XIV
// IV
Enter fullscreen mode Exit fullscreen mode

Other well-known symbols:

  • asyncIterator,
  • match,
  • replace,
  • search,
  • split,
  • hasInstance,
  • isConcatSpreadable,
  • unscopables,
  • species,
  • toPrimitive,
  • toStringTag,

That's all about the Symbols! Now time to practice ;)
Homework

A1. Create custom logger function, which as one of parameter accept one of enum value and data to log. If an invalid value will be passed, throw an error.

// expected result
log(LogLevel.INFO, 'Important information :O');
log(LogLevel.WARN, 'Houston, We Have a Problem!');
log('info', 'Hi!'); // Error: Invalid log level
Enter fullscreen mode Exit fullscreen mode

A2. By default instance of class returns with ToString() [object Object]. But you want to return some, more looking nice name! Create a Logger class. Move function from first exercise inside. Override getter for a Symbol.toStringTag property of the class and return 'Logger' instead.

// expected result
console.log((new Logger()).toString()); // [object Logger]
Enter fullscreen mode Exit fullscreen mode

Want more knowledge and exercises? Follow me on Dev.to and stay tuned!

meatboy image

Top comments (2)

Collapse
 
miteshkamat27 profile image
Mitesh Kamat

Nice explanation.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.