Poznajemy struktury danych w JS takie jak WeakMap oraz WeakSet. Uczymy się nie tylko teorii, ale także analizujemy ciekawe i kreatywne przykłady użycia w OOP tych struktur (przykłady z internetu). Do dzieła.

Ok, zarówno WeakMap jak i WeakSet są dla typów złożonych i obiekty są usuwane z nich (garbage collector) gdy nie ma już do nich referencji.

Przykład z MDN WeakSet:

const ws = new WeakSet();
const foo = {};
const bar = {};

ws.add(foo);
ws.add(bar);

ws.has(foo); // true
ws.has(bar); // true

ws.delete(foo); // removes foo from the set
ws.has(foo); // false, foo has been removed
ws.has(bar); // true, bar is retained

MDN podaje, że za ich pomocą możemy uniknąć np. circular reference:

function execRecursively(fn, subject, _refs = new WeakSet()) {
  // Avoid infinite recursion
  if (_refs.has(subject)) {
    return;
  }

  fn(subject);
  if (typeof subject === "object" && subject) {
    _refs.add(subject);
    for (const key in subject) {
      execRecursively(fn, subject[key], _refs);
    }
    _refs.delete(subject);
  }
}

const foo = {
  foo: "Foo",
  bar: {
    bar: "Bar",
  },
};

foo.bar.baz = foo; // Circular reference!
execRecursively((obj) => console.log(obj), foo);

To robi console loga na obiekcie foo, potem pokazuje wartość property foo (Foo), potem loguje obiekt bar, potem wartość property bar (Bar). Baz będący referencją kołową jest omijany.

Oczywiście sami możemy sobie nasz callback dostosować, np. aby logował tylko obiekty proste.

Ale mam lepszy przykład WeakSeta:

const people = new WeakSet();

class Person {
    constructor(firstName, lastName) {

        this.firstName = firstName;
        this.lastName = lastName;

        people.add(this);

    }

    sayHello() {

        if ( !people.has(this) ) {
            throw new TypeError("Person.prototype.sayHello wywołana na niekompatybilnym obiekcie");
        }

        return `${this.firstName} ${this.lastName}`;

    }
}

let person4 = new Person("Jan", "Kowalski");

console.log( person4.sayHello() );

Co to robi? Zaraz się przekonamy:

person4 = null;

let person5 = {
    firstName: "Anna",
    lastName: "Nowak"
};

console.log( Person.prototype.sayHello.call(person5) ); //nie można

Generalnie chodzi o to, że przy każdym odpaleniu konstruktora „po Bożemu” to obiekt jest dodawany do WeakSetu. Nie jest dodawany, gdy tworzymy go w inny sposób (czyli de facto tworzymy jakiś inny obiekt).

Dzięki temu można sprawdzić, czy dana metoda jest wywoływana na „prawidłowym” obiekcie, czy też jakiś obiekt próbuje ją sobie pożyczyć przez call. Jeśli to drugie – będzie TypeError.

Ok, a WeakMap? To taka sama odmiana Map dla obiektów i z garbabe collection.

Przykłady z MDN akurat najlepsze tutaj nie są, ale mam coś takiego:

const Person = (function() {

    const privateData = new WeakMap();

    return class Person {

        constructor(firstName, lastName) {
            privateData.set(this, {
                firstName,
                lastName
            });
        }

        sayHello() {
            let data = privateData.get(this);

            return `${data.firstName} ${data.lastName}`;
        }

    };

})();


let person3 = new Person("Jan", "Kowalski");

console.log(person3.firstName);

console.log( person3.sayHello() );

I tak, WeakMapa schowana przez closure, przez co jest możliwa prywatność pewnych zmiennych. Kluczem obiekt, WeakMapa zamiast mapy, więc i garbage collection zadziała.

A co z prywatnością WeakSetu z poprzedniego przykładu?

Cóż, dzięki modułom możemy to mieć:

const people = new WeakSet();

export default class Person {

//(...)

I już. Enkapsulacja, garbage collection, i różnego rodzaju myki, dzięki którym nawet czysty JS wygląda przyzwoicie w kontekście OOP…