Y
Published on

Well-Known Symbols in JavaScript A Comprehensive Guide

Authors
  • avatar
    Name
    Yinhuan Yuan
    Twitter

Introduction

JavaScript introduced symbols in ECMAScript 2015 (ES6) as a new primitive type. Among these, a special category known as "well-known symbols" plays a crucial role in metaprogramming and customizing object behavior. In this guide, we'll explore these well-known symbols and how they can be used to tap into JavaScript's internal language behaviors.

What are Well-Known Symbols?

Well-known symbols are predefined symbol values that are used as keys for certain object properties. They allow you to hook into core language behaviors and customize them for your objects. These symbols are properties of the Symbol object.

Let's dive into each well-known symbol and see how they can be used:

1. Symbol.iterator

This symbol specifies the default iterator for an object. It's used by for...of loops.

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

for (let value of myIterable) {
  console.log(value)
}
// Output: 1, 2, 3
2. Symbol.asyncIterator

This symbol specifies the default AsyncIterator for an object. It's used by for await...of loops.

const myAsyncIterable = {
  async *[Symbol.asyncIterator]() {
    yield await Promise.resolve(1)
    yield await Promise.resolve(2)
    yield await Promise.resolve(3)
  },
}

;(async () => {
  for await (let value of myAsyncIterable) {
    console.log(value)
  }
})()
// Output: 1, 2, 3
3. Symbol.toStringTag

This symbol is used to create the default string description of an object. It's accessed internally by Object.prototype.toString().

class MyClass {
  get [Symbol.toStringTag]() {
    return 'MyClass'
  }
}

console.log(Object.prototype.toString.call(new MyClass()))
// Output: [object MyClass]
4. Symbol.toPrimitive

This symbol is used to convert an object to a primitive value.

const obj = {
  [Symbol.toPrimitive](hint) {
    if (hint === 'number') {
      return 42
    }
    if (hint === 'string') {
      return 'hello'
    }
    return true
  },
}

console.log(+obj) // Output: 42
console.log(`${obj}`) // Output: hello
console.log(obj + '') // Output: true
5. Symbol.isConcatSpreadable

This symbol is used to configure if an object should be flattened to its array elements when using Array.prototype.concat().

const arr = [1, 2]
const obj = { length: 2, 0: 3, 1: 4, [Symbol.isConcatSpreadable]: true }

console.log([].concat(arr, obj))
// Output: [1, 2, 3, 4]
6. Symbol.species

This symbol specifies a constructor function that is used to create derived objects.

class MyArray extends Array {
  static get [Symbol.species]() {
    return Array
  }
}

const a = new MyArray(1, 2, 3)
const mapped = a.map((x) => x * x)

console.log(mapped instanceof MyArray) // false
console.log(mapped instanceof Array) // true
7. Symbol.match, Symbol.replace, Symbol.search, Symbol.split

These symbols are used to define custom matching, replacing, searching, and splitting behaviors on objects when used with corresponding string methods.

const caseInsensitive = {
  [Symbol.match](str) {
    return this.toLowerCase() === str.toLowerCase()
  },
}

console.log('hello'.match(caseInsensitive)) // null
console.log('HELLO'.match(caseInsensitive)) // true
8. Symbol.hasInstance

This symbol is used to determine if a constructor object recognizes an object as its instance.

class MyClass {
  static [Symbol.hasInstance](instance) {
    return Array.isArray(instance)
  }
}

console.log([] instanceof MyClass) // true
console.log({} instanceof MyClass) // false
9. Symbol.unscopables

This symbol is used to specify object properties that should be excluded from a with environment bindings.

const obj = {
  foo: 1,
  bar: 2,
}

obj[Symbol.unscopables] = {
  foo: true,
}

with (obj) {
  console.log(bar) // 2
  console.log(foo) // ReferenceError: foo is not defined
}

Conclusion

Well-known symbols provide powerful ways to customize object behavior and hook into JavaScript's internal operations. They're a key part of JavaScript's metaprogramming capabilities, allowing developers to create more flexible and expressive code.

When using well-known symbols, remember that they're meant to integrate with JavaScript's existing behaviors. Use them when you need to customize how your objects interact with built-in language features.

As you continue to explore JavaScript, keep these well-known symbols in mind. They can be powerful tools when used appropriately in your code.