Pisząc wczorajszy artykuł, rzuciłem mimochodem zdanie, które było nieprawdziwe i dopiero idąc na spacerze sobie to uświadomiłem. Koniec końców pomyślałem, że to dobry pomysł na kolejny wpis dotykający ciekawego tematu.

Ok, najpierw kod, który omawialiśmy:

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}`;
        }

    };

})();

W pośpiechu rzuciłem coś w stylu, że prywatność to nie tylko closures, bo są jeszcze moduły i w modułach możemy mieć top-level zmienne, których nie eksportujemy.

Tak, to prawda, ale te zmienne byłyby współdzielone przez wszystkie instancje klasy zdefiniowanej i eksportowanej w tym pliku. Oczywiście prywatność możemy osiągnąć przez #, mamy też TS, w którym mamy prywatne zmienne, są też inne bajery, ale nie ma takiej opcji, aby za pomocą top-level zmiennej osiągnąć prywatne pola, które są osobne dla każdej instancji obiektu.

Zresztą, jak to z tą obiektówką w JS też już sobie mówiliśmy. Ale takie zmienne, prywatne przez moduł i współdzielone przez wszystkie instancje obiektów w danej klasie też mogą się do czegoś przydać.

Tutaj za przykład podam bibliotekę eQuery. Jest to biblioteka robiona z pewnym kursem internetowym i każdy kto ją zrobił ma prawo ją na Github wrzucić (sami autorzy też chyba to zrobili). Zazwyczaj staram się wam podać wyszperane kody z internetu, niekoniecznie kody, na których ja się programować uczyłem, ale zrobimy czasami wyjątek, zwłaszcza jak potrzebujemy jakiegoś bardzo dobrego i konkretnego przykładu.

I DOM Wrapper jest takim przykładem. Generalnie całe flow będzie wyglądało tak:

  • Plik z type module, który importuje funkcje do obsługi selektorów i odpowiednio przeszukiwania DOM albo tworzenia elementów i dopisuje pod window.eQuery funkcję init, która tworzy klasę eQuery jej statyczną metodą przekazując jej nodes
  • Plik z klasą eQuery, która prywatnie (przez moduł) ma WeakMap nodes zaś eksponuje przez eksport klasę eQuery ze statyczną metodą create oraz różnymi metodami
  • Klasa eQuery robi extends mixin, mixin to funkcja, która tworzy fn, do jego prototypu dopisuje wszystkie przekazane funkcje, i zwraca jeden obiekt, z którego można dziedziczyć (tak, mixiny w czystym JS są takie proste!)
  • Mixiny to są obiekty zawierające różne metody, pogrupowane względem przeznaczenia, ładny, czysty, modularny kod

Ok, to nasza funkcja init. Dodam, omawiamy pewien mechanizm, więc tworzenia nodes/przeszukiwania DOM nie będziemy tutaj pokazywać:

import eQuery from "./Library/eQuery.js";
import { isSelector, isHTMLTag, isDOMNode } from "./Utils/Check.js";
import { findElements, createElement } from "./Utils/Element.js";

function init(param) {

    let nodes = null;

    if( isSelector(param) ) {
        nodes = findElements(param);
    } else if( isHTMLTag(param) ) {
        nodes = createElement(param);
    } else if( isDOMNode(param) ) {
        nodes = param;
    }

    return eQuery.create(nodes);

}

window.eQuery = init;

Ok, to zobaczmy, jak wygląda klasa eQuery oraz gdzie te nodes (utworzone bądź złapane selektorem elementy DOM) lądują:

import Attributes from "./Attributes.js";
import Iteration from "./Iteration.js";
import Content from "./Content.js";
import Events from "./Events.js";
import { mixin } from "../Utils/Mixin.js";

const _NODES = new WeakMap();

class eQuery extends mixin(Attributes, Iteration, Content, Events) {

    constructor(nodes) {

        super();

        if( !Array.isArray(nodes) ) {
            nodes = [nodes];
        }

        _NODES.set(this, nodes);

    }

    get(index) {
        let nodes = _NODES.get(this);

        if( Number.isInteger(index) ) {
            return nodes[index];
        } else {
            return nodes;
        }
    }

    *[Symbol.iterator]() {
        yield *this.get();
    }

    static create(nodes) {
        return new eQuery(nodes);
    }

}

export default eQuery;

Genialne, prawda? To zobaczmy jak wygląda mixin:

export function mixin(...mixins) {

    const fn = function() {};

    Object.assign(fn.prototype, ...mixins);

    return fn;

}

Już to tłumaczyliśmy, więc tylko powiem, że jeszcze bardziej genialne, z podziwu nie mogę wyjść! Jasne, własnego MRO tu nie napisaliśmy, to nie multidziedziczenie jak w Pythonie czy C++, ale jak niewiele trzeba, aby mieć mixiny w JS.

Jeżeli robiliśmy klasy w ES5 to tam dopisywaliśmy metody do prototypu. Prototypu funkcji konstruującej (do this przypisując to każdy obiekt będzie miał te definicje, które powinny być współdzielone).

A co robi object assign? A czym jest klasa w JS? Czym jest obiekt i funkcja? Jedyne, co możemy nie ogarniać, to jaka „czarna magia” kryje się za słowem extends, ale skoro to działa, to już chyba wiemy, jaka.

Taki mixin wygląda tak:

export default {

    text(value) {
        if(value !== undefined) {
            return this.each( node => node.textContent = value );
        } else {
            return this.get(0).textContent;
        }
    },

    html(value) {
        if(value !== undefined) {
            return this.each( node => node.innerHTML = value );
        } else {
            return this.get(0).innerHTML;
        }
    }

};

Głównym celem tej lekcji było pokazanie, że można zrobić weakmap powyżej klasy i nie eksportować tego weakmap, a klasę tak plus pokazać ten flow od main.js do eQuery.js, także uwrażliwić, że moduły zapewniają prywatność, ale aby była prywatność niedzielona przez wszystkie instancje, to potrzebne jest closure.

Natomiast te mixiny to też jest coś pięknego i teraz mamy taki schemat jak pisać różnego rodzaju biblioteczki w czystym JS, aby ten kod ładnie wyglądał, był modularny i jak na czysty JS całkiem uporządkowany.

Do następnego razu!