Hopp til innholdet

cjohansen.no

Mixins og multippel arv i JavaScript

Fordi metoder er helt vanlige egenskaper på objekter er det fullt mulig å dele metoder fritt mellom objekter. På denne måten støtter JavaScript indirekte multippel arv fordi ett objekt kan "arve" oppførsel fra flere enn ett annet objekt.

I gårsdagens tutorial (og den første om $()) har vi sett ett eksempel på hvordan to objekter kan dele på en metode. Vi definerte metoder inn i cj.Element som kunne kalles direkte fra dette navnerommet. Deretter laget vi vår extend-metode som laget en anonym funksjon rundt hver og én av disse metodene for bruk gjennom et elements prototype.

Å legge metoder rundt på denne måten er én måte å dele metoder på, som også tillater deg å endre noe på grensesnittet. I vårt eksempel var det snakk om å unngå element-parameteret (som vi allerede hadde innebygget fra et gitt element).

Noen ganger kan det være interessant å kopiere metoder rett over uten å endre på grensesnittet. I disse tilfellene kan vi rett og slett gjøre:

String.prototype.isLongerThan = function(len) {
    return this.length > len;
};

console.log("Hey there".isLongerThan(3)); // true

Array.prototype.isLongerThan = String.prototype.isLongerThan;

console.log(["Hey", "there"].isLongerThan(3)); // false

Her utvider vi først prototypen til strenger med en tullete metode som sjekker om strengen er lengre enn en gitt lengde. Deretter kopierer vi metoden verbatim over i array sin prototype og får umiddelbart samme funksjonalitet på arrayer. Dette fungerer fordi både arrayer og strenger har en length-egenskap.

Mixins

De av dere som har erfaring med Ruby, Python, Perl og diverse andre språk vil sikkert notere seg at vi i eksempelet over touchet innom noe som ligner på mixins. JavaScript har ikke støtte for Mixins på språknivå, men å få full mixin-støtte er alikevel bare en metode unna. Faktisk kan vi bruke extend-metoden som vi laget tidligere. Alt den trenger å gjøre er å kopiere egenskaper fra ett objekt til et annet.

Jeg minner om hvordan den fungerer:

var object = { a: 1 };
object = extend(object, { b: 2 });
// object == { a: 1, b: 2 }

Det som er kult med mixins er at vi kan gi objekter funksjonalitet uten å tvinge "klassen" til å subklasse andre klasser. Med andre ord kan vi si noe direkte om hva en gruppe objekter kan fremfor hva de er.

I Ruby brukes for eksempel mixins til å gi forskjellige objekter et bredt spekter av enumerable-funksjonalitet (norsk ord anyone??). Såfremt en klasse definerer en instansmetode each som gir underelementer én etter en så vil Enumerable-mixinen gi disse objektene andre nyttige metoder så som all?, min og sort. De to siste krever også at enkeltverdiene har definert en sammenligningsmetode.

La oss se et eksempel på dette i JavaScript. Først, la oss se et eksempel på iteratoren, som i JavaScript for arrayer heter forEach:

if (!Array.prototype.forEach) {
    Array.prototype.forEach = function(fun /*, thisp*/) {
        var len = this.length;

        if (typeof fun != "function")
            throw new TypeError();

        var thisp = arguments[1];

        for (var i = 0; i < len; i++) {
            if (i in this)
                fun.call(thisp, this[i], i, this);
        }
    };
}

Dette er Mozillas implementasjon som vi har sett før. Den kan brukes som så:

var arr = [2, 3, 4, 1];

arr.forEach(function(element) {
    console.log(element);
});

La oss nå lage noen enumerable-metoder som benytter seg av denne iteratoren. Jeg illustrerer som vanlig først bruk med noen tester, så presenterer jeg en implementasjon.

new Test.Unit.Runner({
    testIsMixedIntoArrays: function() { with(this) {
        var array = [1, 2, 3, 4];
        assertNotUndefined(Array.prototype.all);
        assertNotUndefined(array.all);
    }},

    testAll: function() { with(this) {
        var array = [1, 2, 3, 4];
        assert(!array.all(function(num) { return num > 1; }));
        assert(array.all(function(num) { return num > 0; }));
    }},

    testAny: function() { with(this) {
        var array = [1, 2, 3, 4];
        assert(array.any(function(num) { return num > 1; }));
        assert(array.any(function(num) { return num > 0; }));
        assert(!array.any(function(num) { return num === 0; }));
    }},

    testCollect: function() { with(this) {
        var array = [document.getElementById("list"), document.getElementById("something")];

        assertEnumEqual(["ul", "div"], array.collect(function(element) {
            return element.tagName.toLowerCase();
        }));
    }}
}, { testLog: "enumerablelog" });

Som forventet feiler testene. Så var det implementasjonen.

var cj = {};

cj.Enumerable = {
    // Error type used to abort forEach callbacks
    StopIteration: function() {},

    /**
     * Returns true if the callback never returns a falsy value
     */
    all: function(callback) {
        try {
            this.forEach(function(element) {
                if (!callback(element)) {
                    throw new StopIteration();
                }
            });
        } catch(err) {
            return false;
        }

        return true;
    },

    /**
     * Returns true if the callback ever returns a thruthy value
     */
    any: function(callback) {
        try {
            this.forEach(function(element) {
                if (!!callback(element)) {
                    throw new StopIteration();
                }
            });
        } catch(err) {
            return true;
        }

        return false;
    },

    /**
     * Returns a new array with the results of running the callback
     * once for every element in the collection.
     */
    collect: function(callback) {
        var newEnum = [];

        this.forEach(function(element) {
            newEnum.push(callback(element));
        });

        return newEnum;
    }
};

Dette er tre av metodene som Rubys enumerable tilbyr. Merk at siden vi looper samlingene med callbacks er exceptions den eneste måten vi kan avbryte en loop på. For all-metoden vet vi for eksempel at vi ikke trenger å loope mer så fort vi får en ikke-sann verdi fra callback-metoden. Tilsvarende kan vi avbryte så fort vi har fått en sann verdi for any.

Det eneste vi mangler nå er å legge til denne funksjonaliteten for arrayer. Dette kan vi enkelt oppnå med extend-metoden, som for anledningen har fått aliaset mixin:

mixin(Array.prototype, cj.Enumerable);

Merk aliaset mixin. Dette er en enkel sak:

var mixin = extend;

Testene våre bekrefter at denne implementasjonen duger. For arrayer. Hva med andre samlinger?

Styrken med mixins

Styrken med mixins viser seg når vi nå mikser inn modulen vår i en nodeliste. En av testene våre så ut som:

testCollect: function() { with(this) {
    var array = [document.getElementById("list"), document.getElementById("something")];

    assertEnumEqual(["ul", "div"], array.collect(function(element) {
        return element.tagName.toLowerCase();
    }));
}}

Legg merke til at "list" og "something" er idene på to elementer som kunne vært hentet med document.getElementById("sample").getElementsByTagName("*");. Denne metoden returnerer et NodeList-objekt. La oss gi NodeList-objektet en forEach-metode, og deretter all funksjonalitet i cj.Enumerable. Det fungerer desverre ikke å bare legge funksjonalitet på NodeList.prototype som vi er vant til, så vi må mikse inn funksjonaliteten på enkelte objekter.

testCollectNodeList: function() { with(this) {
    var elements = document.getElementById("sample").getElementsByTagName("*");
    elements = enumerableNodeList(elements);

    assertEnumEqual(["ul", "div"], elements.collect(function(element) {
        return element.tagName.toLowerCase();
    }));
}}

Testen feiler selvsagt, så la oss skrive litt kode. Fordi vi ikke kan utvide prototypen som vi pleier har vi lagt opp til en metode som returnerer en enumerable nodeliste. Implementasjonen av Array.prototype.forEach er såppass generisk at vi kan kopiere den direkte over på nodelisten:

function enumerableNodeList(list) {
    // Already extended
    if (list.all) {
        return list;
    }

    // Borrow arrays foreach iterator
    list.forEach = Array.prototype.forEach;

    // Return extended object
    return mixin(list, cj.Enumerable);
}

Mer skulle ikke til.

Dersom du liker det du ser kan jeg informere om at prototype.js har en full Enumerable-implementasjon som ligner på den vi har sett her idag.

Vel møtt i morgen for en diskusjon av debugging og profilering av JavaScript.

Muligens relatert

2006 - 2010 Christian Johansen Creative Commons Lisens