23. februar 2026

Decorators og videresendelse, call/apply

JavaScript giver enestående fleksibilitet, når det kommer til funktioner. De kan sendes rundt, bruges som objekter, og nu vil vi se, hvordan man videresender kald mellem dem og dekorerer dem.

Transparent caching

Lad os sige, at vi har en funktion slow(x), som er CPU-tung, men dens resultater er stabile. Med andre ord returnerer den altid det samme resultat for den samme x.

Hvis funktionen kaldes ofte, kan vi ønske at cache (huske) resultaterne for at undgå at bruge ekstra tid på genberegninger.

Men i stedet for at tilføje den funktionalitet direkte i slow(), vil vi oprette en wrapper-funktion, der tilføjer caching. Som vi vil se, er der mange fordele ved at gøre det.

Her er koden. Forklaringen følger nedenfor:

function slow(x) {
  // her kan der være et CPU-tungt job
  alert(`Kaldt med ${x}`);
  return x;
}

function cachingDecorator(func) {
  let cache = new Map();

  return function(x) {
    if (cache.has(x)) {    // hvis der er sådan en nøgle i cache
      return cache.get(x); // læs resultatet fra cache
    }

    let result = func(x);  // ellers kald func

    cache.set(x, result);  // og "cache" (husk) resultatet
    return result;
  };
}

slow = cachingDecorator(slow);

alert( slow(1) ); // slow(1) er gemt i cache og resultatet returneres
alert( "Again: " + slow(1) ); // slow(1) resultatet returnes fra cache

alert( slow(2) ); // slow(2) er gemt i cache og resultatet returneres
alert( "Again: " + slow(2) ); // slow(2) resultatet returnes fra cache

I koden ovenfor er cachingDecorator en decorator: en speciel funktion, der tager en anden funktion og ændrer dens adfærd.

Ideen er at vi kalder cachingDecorator for en hvilken som helst funktion, og den returnerer en caching-wrapper. Det er godt, fordi vi kan have mange funktioner, der kunne bruge denne funktionalitet, og alt hvad vi behøver at gøre er at anvende cachingDecorator på dem.

Ved at separere caching fra hovedfunktionens kode holder vi også hovedkoden enkel.

Resultatet af cachingDecorator(func) er en “wrapper”: function(x) som “wrapper” (omkranser) kaldet af func(x) i caching-logik:

Set udefra vil den nye “wrapped” funktion opføre sig som den originale funktion, men med tilføjet caching-funktionalitet.

Kort sagt, der er flere fordele ved at bruge en separat cachingDecorator i stedet for at ændre koden i slow selv:

  • cachingDecorator er genbrugelig. Vi kan anvende den på en anden funktion.
  • Caching-logikken er separat, den øgede ikke kompleksiteten i slow selv (hvis der var nogen).
  • Vi kan kombinere flere decorators hvis det er nødvendigt (andre decorators vil følge).

Brug “func.call” for at få kontekst

Caching decoratoren nævnt ovenfor er ikke egnet til at arbejde med objektmetoder.

For eksempel, i koden nedenfor stopper worker.slow() efter dekoration med en fejl:

// Vi laver worker.slow om til en udgave med caching
let worker = {
  someMethod() {
    return 1;
  },

  slow(x) {
    // CPU-tung opgave her
    alert("Kaldt med " + x);
    return x * this.someMethod(); // (*)
  }
};

// samme kode som før
function cachingDecorator(func) {
  let cache = new Map();
  return function(x) {
    if (cache.has(x)) {
      return cache.get(x);
    }
    let result = func(x); // (**)
    cache.set(x, result);
    return result;
  };
}

alert( worker.slow(1) ); // den oprindelige metode virker

worker.slow = cachingDecorator(worker.slow); // lad os lave den om til en udgave med cache

alert( worker.slow(2) ); // Ups! Error: Cannot read property 'someMethod' of undefined

Fejlen sker i linjen med (*) der prøver at tilgå this.someMethod men fejler. Kan du se hvorfor?

Grunden til det er at wrapperen kalder den originale funktion som func(x) i linjen (**). Og, når den bliver kaldt på den måde, får funktionen this = undefined.

Vi vil se det samme ske hvis vi prøvede at køre:

let func = worker.slow;
func(2);

Så wrapperen videregiver kaldet til den originale metode, men uden konteksten this. Derfor fejler det.

Lad os fikse det.

Der er en indbygget funktion kaldet func.call(context, …args) der er skabt specielt til lejligheden. Den tillader at kalde en funktion eksplicit og sætte this.

Syntaksen er:

func.call(context, arg1, arg2, ...)

Den kører func og giver det en henvisning til this med som første argument, og derefter eventuelt de næste argumenter.

Sat lidt simpelt op gør disse to kald næsten det samme:

func(1, 2, 3);
func.call(obj, 1, 2, 3)

De kalder begge func med argumenterne 1, 2 and 3. Den eneste forskel er at func.call også sætter this til obj.

I koden nedenfor kalder vi sayHi med forskellige objekter som kontekst: sayHi.call(user) kører sayHi men leverer this=user som kontekst mens den næste linje sætter this=admin:

function sayHi() {
  alert(this.name);
}

let user = { name: "John" };
let admin = { name: "Admin" };

// brug call for at videregive forskellige objekter som "this"
sayHi.call( user ); // John
sayHi.call( admin ); // Admin

og her bruger vi call til at kalde say med den givne kontekst og frase:

function say(phrase) {
  alert(this.name + ': ' + phrase);
}

let user = { name: "John" };

// user bliver til this og "Hej" bliver det første argument
say.call( user, "Hej" ); // John: Hej

I vores oprindelige eksempel kan vi bruge call i wrapperen til at videregive konteksten til den originale funktion:

let worker = {
  someMethod() {
    return 1;
  },

  slow(x) {
    alert("Kaldt med " + x);
    return x * this.someMethod(); // (*)
  }
};

function cachingDecorator(func) {
  let cache = new Map();
  return function(x) {
    if (cache.has(x)) {
      return cache.get(x);
    }
    let result = func.call(this, x); // "this" liver sendt rigtigt nu
    cache.set(x, result);
    return result;
  };
}

worker.slow = cachingDecorator(worker.slow); // omdan den til en version med cache

alert( worker.slow(2) ); // virker
alert( worker.slow(2) ); // virker, kalder ikke originalen med den cachede version

Nu virker alt fint.

For at gøre det helt tydeligt, lad os gå mere i dybden med hvordan this videregives i det sidste eksempel:

  1. Efter dekorationen af worker.slow er den nu omdannet til wrapperen function (x) { ... }.
  2. Så når worker.slow(2) eksekveres vil wrapperen få 2 som argument og this=worker (det er objektet før punktum).
  3. Inde i wrapperen, forudsat at det ikke er i chachen, vil func.call(this, x) videregive den aktuelle this (=worker) og det aktuelle argument (=2) til den originale metode.

Hvad med flere argumenter?

Lad os gøre cachingDecorator endnu mere universiel. Indtil videre fungerer den kun med funktioner med ét argument.

Hvordan skal vi cache en multi-argument worker.slow metode?

let worker = {
  slow(min, max) {
    return min + max; // Skræmmende tung CPU-opgave her
  }
};

// skal huske kald med de samme argumenter
worker.slow = cachingDecorator(worker.slow);

Tidligere kunne vi for et enkelt argument x bare kalde cache.set(x, result) for at gemme resultatet og cache.get(x) for at hente det. Men nu skal vi huske resultatet for en kombination af argumenter (min,max). Den indbyggede Map tager kun en enkelt værdi som nøgle.

Der er flere mulige løsninger:

  1. Implementer en ny (eller brug en 3de-parts) map-lignende datastruktur der er mere fleksibel og tillader brug af flere nøgler.
  2. Brug indlejrede maps: cache.set(min) vil bleve et Map der gemmer parret (max, result). På den måde kan vi hente resultatet som cache.get(min).get(max).
  3. Forbind to værdier til et. I vores tilfælde vil vi kunne bruge en simpel streng "min,max" som Map-nøgle. For fleksibilitet kan vi levere en hashing funktion til vores decorator, der ved hvordan man laver en værdi ud af mange.

For de fleste praktiske anvendelser er variant 3 god nok, så vi holder os til den.

Endelig skal vi huske at vi ikke bare skal sende x, men alle argumenter i func.call. Lad os huske at i en function() kan vi få et pseudo-array af dens argumenter som arguments, så func.call(this, x) bør erstattes med func.call(this, ...arguments).

Her er en mere kraftfuld cachingDecorator:

let worker = {
  slow(min, max) {
    alert(`Called with ${min},${max}`);
    return min + max;
  }
};

function cachingDecorator(func, hash) {
  let cache = new Map();
  return function() {
    let key = hash(arguments); // (*)
    if (cache.has(key)) {
      return cache.get(key);
    }

    let result = func.call(this, ...arguments); // (**)

    cache.set(key, result);
    return result;
  };
}

function hash(args) {
  return args[0] + ',' + args[1];
}

worker.slow = cachingDecorator(worker.slow, hash);

alert( worker.slow(3, 5) ); // virker, slow(3, 5) er gemt i cache
alert( "Igen " + worker.slow(3, 5) ); // det samme her (cachet resultat returneres)

Nu virker det med et givet antal argumenter (selvom hashing-funktionen også skal justeres for at tillade et vilkårligt antal argumenter. En interessant måde at håndtere dette vil blive beskrevet nedenfor).

Der er to ændringer i cachingDecorator:

  • I linjen (*) kaldes funktionen hash som opretter en enkelt tekststreng som nøgle fra arguments. Det er en simpel “join” funktion der omdanner argumenterne (3, 5) til strengen "3,5". Mere komplekse tilfælde kan kræve andre hashing-funktioner.
  • Senere i (**) bruger func.call(this, ...arguments) til at videregive både context og alle argumenter som wrapperen fik (ikke kun det første) til den originale funktion.

func.apply

I stedet for func.call(this, ...arguments) kunne vi bruge func.apply(this, arguments).

Syntaksen for den indbyggede metode func.apply er:

func.apply(context, args)

Den kører func og sætter this=context og bruger et array-lignende objekt args som en liste af argumenter.

Den eneste forskel på syntaksen mellem call og apply er at call forventer en liste af argumenter, mens apply tager et array-lignende objekt med dem.

Så disse to kald er næsten ens:

func.call(context, ...args);
func.apply(context, args);

De udfører det samme kald af func med en givet kontekst og argumenter.

Der er kun en lille forskel i håndtering af args:

  • Spread syntaksen ... tillader at videregive itererbare args som en liste til call.
  • apply tillader kun array-lignende args.

…og for objekter der både er itererbare og array-lignende, så som rigtige arrays, kan vi bruge begge. Men apply vil sikkert være hurtigere, fordi de fleste JavaScript-motorer internt optimerer det bedre.

Et videregive alle argumenter sammen med konteksten til en anden funktion kaldes call forwarding.

Dette er den simpleste form af det:

let wrapper = function() {
  return func.apply(this, arguments);
};

Når en ekstern kode kalder en sådan wrapper, er det ikke muligt at skelne mellem kaldet af denne fra den originale funktion func.

Lån en metode

Lad os nu lave en mindre forbedring til hashing-funktionen.

function hash(args) {
  return args[0] + ',' + args[1];
}

Som det er nu virker det kun på to argumenter. Det ville være bedre hvis den kunne sammenkæde et vilkårligt antal args.

Den naturlige løsning vil være at bruge metoden arr.join:

function hash(args) {
  return args.join();
}

…uheldigvis virker det ikke. Det er fordi vi kalder hash(arguments) og arguments objektet er både itererbart og array-lignende – men ikke et rigtigt array.

Så kaldet til join på det vil fejle, som vi ser nedenfor:

function hash() {
  alert( arguments.join() ); // Error: arguments.join is not a function
}

hash(1, 2);

Men, der er et nemt trick til at kunne bruge array join:

function hash() {
  alert( [].join.call(arguments) ); // 1,2
}

hash(1, 2);

Dette trick kaldesmethod borrowing.

Vi tager (låner) join metoden fra et regulært array ([].join) og bruger [].join.call til at køre det i kontekst af arguments.

Hvorfor virker det?

Det er fordi den interne algoritme for metoden arr.join(glue) er ret simpel.

Taget fra specifikationen som det står ordret:

  1. Lad glue være det første argument eller, hvis ingen argumenter, så en komma ",".
  2. Lad result være en tom streng.
  3. Tilføj this[0] til result.
  4. Tilføj glue og this[1].
  5. Tilføj glue og this[2].
  6. …fortsæt indtil this.length elementer er sammenkædet.
  7. Returner result.

Så teknisk set tager den this og kæder this[0], this[1] …osv sammen. Det er bevidst skrevet på en måde der tillader ethvert array-lignende this (ikke en tilfældighed, mange metoder følger denne praksis). Derfor virker det også med this=arguments.

Decorators og egenskaber af funktioner

Det er normalt uproblematisk at erstatte en funktion eller en metode med en dekoreret version, bortset fra ét lille punkt. Hvis den originale funktion havde egenskaber på sig, som func.calledCount eller lignende, så vil den dekorerede funktion tage dem i betragtning. Set i det lys skal man være opmærksom på at kopiere egenskaberne fra den originale funktion til wrapperen, hvis de findes.

E.g. i eksemplet ovenfor, hvis slow funktionen havde nogle egenskaber på sig, så vil cachingDecorator(slow) være en wrapper uden dem.

Nogle dekoratorer kan give deres egne egenskaber. F.eks. kan en dekorator tælle hvor mange gange en funktion blev kaldt og hvor lang tid det tog, og eksponere denne information via wrapper-egenskaber.

Der findes måder at oprette dekoratorer der bevarede adgang til funktionsegenskaber, men det kræver brugen af et specielt Proxy objekt til at omkring en funktion. Vi vil diskutere det senere i artiklen Proxy and Reflect.

Opsummering

Decorator er en wrapper omkring en funktion, som ændrer dens adfærd. Det primære job er stadig udført af den originale funktion.

Decorators kan ses som “features” eller “aspekter” som kan tilføjes til en funktion. Vi kan tilføje én eller flere. Det særlige ved dekoratorer er at alt dette uden at ændre dens kode!

For at implementere cachingDecorator, studerede vi to metoder:

Den generelle call forwarding sker normalt med apply:

let wrapper = function() {
  return original.apply(this, arguments);
};

Vi så også et eksempel på method borrowing hvor vi tager en metode fra et objekt og kalder den med call i kontekst af et andet objekt. Det er ganske almindeligt at tage array-metoder og anvende dem på arguments. Den alternative tilgang er at bruge rest parameters objektet, som er et rigtigt array.

Der findes mange dekoratorer i kodeprojekter på nettet. Tjek hvor godt du fik styr på dem ved at løse opgaverne i dette kapitel.

Opgaver

vigtighed: 5

Opret en decorator spy(func) der returnerer en wrapper der gemmer alle kald til funktionen i dens calls egenskab.

Hvert kald er gemt som et array af argumenter.

For eksempel:

function work(a, b) {
  alert( a + b ); // work er en tilfældig funktion eller metode
}

work = spy(work);

work(1, 2); // 3
work(4, 5); // 9

for (let args of work.calls) {
  alert( 'call:' + args.join() ); // "call:1,2", "call:4,5"
}

P.S. Denne decorator kan være brugbar ved unit-testing. En avanceret form er sinon.spy i Sinon.JS biblioteket.

Åbn en sandbox med tests.

Wrapperen der returneres af spy(f) skal gemme alle argumenter og derefter bruge f.apply til at videregive kaldet.

function spy(func) {

  function wrapper(...args) {
    // bruger ...args i stedet for arguments for at gemme et "rigtigt" array i wrapper.calls
    wrapper.calls.push(args);
    return func.apply(this, args);
  }

  wrapper.calls = [];

  return wrapper;
}

Åbn løsningen med tests i en sandbox.

vigtighed: 5

Opret en decorator delay(f, ms) der forsinker hvert kald af f med ms millisekunder.

For eksempel:

function f(x) {
  alert(x);
}

// Opret wrappers der forsinker kaldet af f med 1000ms og 1500ms
let f1000 = delay(f, 1000);
let f1500 = delay(f, 1500);

f1000("test"); // viser "test" efter 1000ms
f1500("test"); // viser "test" efter 1500ms

Med andre ord: delay(f, ms) returner en udgave af f der er “forsinket med ms millisekunder”.

I koden ovenfor er f en funktion med et enkelt argument, men din løsning skal videregive alle argumenter og konteksten this.

Åbn en sandbox med tests.

Løsningen:

function delay(f, ms) {

  return function() {
    setTimeout(() => f.apply(this, arguments), ms);
  };

}

let f1000 = delay(alert, 1000);

f1000("test"); // viser "test" efter 1000ms

Bemærk hvordan en arrow function er brugt her. Som vi ved, har arrow functions ingen egen this og arguments, så f.apply(this, arguments) tager this og arguments fra wrapperen.

Hvis vi sender en almindelig funktion, vil setTimeout kalde den uden argumenter og med this=window (hvis vi er i browseren).

Vi kan stadig videregive det rigtige this ved at bruge en midlertidig variabel, men det er lidt mere besværligt:

function delay(f, ms) {

  return function(...args) {
    let savedThis = this; // gem this som en midlertidig variabel
    setTimeout(function() {
      f.apply(savedThis, args); // brug den her
    }, ms);
  };

}

Åbn løsningen med tests i en sandbox.

vigtighed: 5

Resultatet af en debounce(f, ms) decorator er en wrapper der suspenderer kaldet til f indtil der er ms millisekunder af inaktivitet (ingen kald, “cooldown period”), så kalder den f én gang med de seneste argumenter.

Med andre ord er debounce som en sekretær der tager imod “telefonopkald” og venter ind til der har været ms millisekunder af inaktivitet. Og først da overfører den de seneste opkaldsoplysninger til “chefen” (kalder den faktiske f).

For eksempel, vi havde en funktion f og erstattede den med f = debounce(f, 1000).

Hvis den omgivende funktion kaldes ved 0ms, 200ms og 500ms, og der ikke er flere kald efter det, vil den faktiske f kun blive kaldt én gang, ved 1500ms. Det vil sige: efter cooldown-perioden på 1000ms fra det sidste kald.

…og vi vil få argumenterne fra det sidste kald, andre kald ignoreres.

Her er koden for det (som bruger debounce decorator fra Lodash biblioteket):

let f = _.debounce(alert, 1000);

f("a");
setTimeout( () => f("b"), 200);
setTimeout( () => f("c"), 500);
// debounced funktion venter 1000ms efter det sidste kald og kalder så alert("c")

Nu til et praktisk eksempel. Lad os sige, at brugeren skriver noget, og vi vil sende en forespørgsel til serveren når inputtet er færdigt.

Der er ingen pointe i at sende forespørgslen for hver tastetryk. I stedet vil vi vente og så behandle hele resultatet.

en webbrowser kan vi opsætte en event handler – en funktion der kaldes ved hver ændring af et inputfelt. Normalt kaldes en event handler meget ofte, for hver tastetryk. Men hvis vi debounce den med 1000ms, vil den kun blive kaldt én gang, efter 1000ms efter det sidste input.

I dette live eksempel sætter handleren resultatet i en boks nedenfor, prøv det:

Kan du se effekten? Det andet input kalder den debounced funktion, så dens indhold behandles efter 1000ms fra det sidste input.

debounce er en god måde at processere en række af events: en sekvens at tastetryk, musbevægelser eller andet.

Den venter en given tid efter det sidste kald, og så kører den sin funktion, som kan behandle resultatet.

Din opgave er at implementere en debounce decorator.

Hint: det er egentlig kun et par linjer hvis man tænker over det :)

Åbn en sandbox med tests.

function debounce(func, ms) {
  let timeout;
  return function() {
    clearTimeout(timeout);
    timeout = setTimeout(() => func.apply(this, arguments), ms);
  };
}

Et kald til debounce returnerer en wrapper. Når den kaldes, planlægger den et kald til den originale funktion efter ms millisekunder og annullerer tidligere timeouts hvis de findes.

Åbn løsningen med tests i en sandbox.

vigtighed: 5

Opret en “throttling” decorator throttle(f, ms) – der returnerer en wrapper.

Når den kaldes flere gange, sender den kaldet til f maksimalt én gang pr. ms millisekunder.

Sammenlignet med debounce decorator er adfæren helt anderledes:

  • debounce kører funktionen én gang efter “cooldown” perioden. Godt til at behandle det endelige resultat.
  • throttle kører den ikke oftere end givet ms tid. Godt til regelmæssige opdateringer der ikke bør ske for ofte.

Med andre ord er throttle en slags sekretær der accepterer telefonopkald uden at ville genere chefen (calls the actual f) mere end én ger per ms millisekunder.

Lad os se på et eksempel fra en realistisk situation for bedre at forstå metoden.

Tracking af musens bevægelser.

I en browser kan vi opsætte en funktion der modtager musens kooridnater og kaldes hver gang musen bevæger sig. I praksis vil sådan en funktion blive kaldt ret ofte – noget i stil med 100 gange i sekundet (hvert 10. millisekund).

Vi vil gerne opdatere informationen på siden når musen bevæger sig.

…Men funktionen update() der skal stå for opdateringen er alt for tung til at køre ved hver mikro-bevægelse. Der giver nok heller ingen mening at opdatere oftere end én gang pr. 100ms.

Derfor pakker vi den ind i en decorator: brug throttle(update, 100) som funktionen der skal køres ved hver mus-bevægelse i stedet for den originale update(). Decoratoren vil blive kaldt ofte, men videregive kaldet til update() maksimalt én gang pr. 100ms.

Det vil se ud i stil med dette:

  1. For den første bevægelse med musen vil den dekorerede variant med det samme videregive kaldet til update. Det er vigtigt, brugeren ser vores reaktion på bevægelsen.
  2. Derefter, mens musen bevæger sig videre og indtil 100ms er gået, sker der intet. Den dekorerede variant ignorerer kaldene.
  3. Ved slutningen af 100ms – en ekstra update sker med de sidste koordinater.
  4. Til sidst, når musen stopper et sted, venter den dekorerede variant til 100ms er udløbet og kører derefter update med de sidste koordinater. Så er det vigtigt at de sidste koordinater bliver behandlet.

Et kodet eksempel:

function f(a) {
  console.log(a);
}

// f1000 videregiver kald til f maks én gang per 1000 ms
let f1000 = throttle(f, 1000);

f1000(1); // vider 1
f1000(2); // (throttling, 1000ms ikke endnu)
f1000(3); // (throttling, 1000ms ikke endnu)

// når 1000 ms er gået...
// ...output'er 3, den midterste værdi 2 ignoreres

P.S. Argumenterne og konteksten this der gives til f1000 skal videregives til den originale f.

Åbn en sandbox med tests.

function throttle(func, ms) {

  let isThrottled = false,
    savedArgs,
    savedThis;

  function wrapper() {

    if (isThrottled) { // (2)
      savedArgs = arguments;
      savedThis = this;
      return;
    }
    isThrottled = true;

    func.apply(this, arguments); // (1)

    setTimeout(function() {
      isThrottled = false; // (3)
      if (savedArgs) {
        wrapper.apply(savedThis, savedArgs);
        savedArgs = savedThis = null;
      }
    }, ms);
  }

  return wrapper;
}

Et kald til throttle(func, ms) returnerer wrapper.

  1. Ved første kald kører wrapper funktionen func og sætter cooldown-tilstanden (isThrottled = true).
  2. I denne tilstand gemmes alle kald i savedArgs/savedThis. Bemærk at både konteksten og argumenterne er lige så vigtige og skal gemmes. Vi har brug for begge dele samtidigt for at kunne genskabe kaldet.
  3. Efter ms millisekunder har gået, udløser setTimeout. Cooldown-tilstanden fjernes (isThrottled = false) og, hvis der var ignorerede kald, køres wrapper med de sidste gemte argumenter og kontekst.

Det tredje trin kører ikke func, men wrapper, fordi vi ikke kun skal køre func, men også igen indtaste cooldown-tilstanden og opsætte timeout’en til at nulstille den.

Åbn løsningen med tests i en sandbox.

Tutorial-oversigt

Kommentarer

læs dette før du kommenterer…
  • Hvis du har forslag til forbedringer - så opret venligst et GitHub-issue eller en pull request i stedet for at kommentere.
  • Hvis du ikke forstår noget i artiklen - så uddyb venligst.
  • For at indsætte få ord kode, brug <code>-taggen, for flere linjer - omslut dem i <pre>-tag, for mere end 10 linjer - brug en sandbox (plnkr, jsbin, codepen…)