JS Tip of the Day: Internal Slots

Internal Slots
Level: Advanced

JavaScript maintains hidden properties within your objects known as internal slots. These are used internally by the runtime for things like managing how the object behaves, or what kind of data it it might store internally. Internal slots are usually represented by a property name surrounded by two square brackets ([[<name>]]) but they are not directly accessible as properties to JavaScript code. For those that can be accessed from JavaScript, you’d need to go through an alternative API.

An object’s prototype is an example of something that is stored in an internal slot. When an object needs to see what it inherits, it looks to its internal [[Prototype]] which is the internal slot that stores its prototype object. While user code can’t access [[Prototype]] directly, there are APIs like Object.getPrototypeOf() and the __proto__ getter which will provide its value to JavaScript.

let map = new Map(); // map.[[Prototype]] = Map.prototype
let proto = Object.getPrototypeOf(map); // returns map.[[Prototype]]
console.log(proto === Map.prototype); // true

Internal slots usually hide in the background allowing you to ignore their existence as they secretly work to ensure your objects behave as they should. But sometimes the nature of internal slots work can cause unexpected problems.

Unlike normal object properties, internal slots are not inherited. If you create an object that inherits from another object with a certain internal slot, that slot will not be accessible from the new object. Map instances, like map in the example above, use an internal slot called [[MapData]] (which you may see represented as [[Entries]] in developer tooling) to store their key-value pairs. If you created an object that inherited from map, Map methods of that object would fail because of the lack of [[MapData]] in that new object.

let map = new Map([['Home', '37.7N-122.5W']]);
console.log(map.get('Home')); // 37.7N-122.5W

let fakeMap = Object.create(map); // new fakeMap object inherits from map
console.log(fakeMap.get('Home'));
// TypeError: incompatible receiver

Maps aren’t the only type that suffer from this problem. There are a number of other objects that would behave similarly, including, but not limited to:

Object.create(new Set()).size; // TypeError
Object.create(new Date()).getTime(); // TypeError
Object.create(new Number(1)).valueOf(); // TypeError
Object.create(Promise.resolve(1)).then(() => {}); // TypeError

While you may not be able to create working objects that inherit from instances of these types, using class syntax, you are able to subclass them.

class FakeMap extends Map {
    constructor (entries) {
        super(entries); // initializes this instance with Map internal slots
    }
}

let fakeMap = new FakeMap([['Home', '37.7N-122.5W']]);
console.log(fakeMap.get('Home')); // 37.7N-122.5W (not so fake anymore)

This version of fakeMap doesn’t fail when calling the Map get method because it was initialized with the proper internal slot expected by that method (and others like it) having gone through the FakeMap, and therefore Map, constructor.

More info: