arguments, apply, call und der Debugger – Programmieren lernen mit JavaScript – Thytos
Nächstes Video startet in 3 Sekunden.
Programmieren lernen mit JavaScript

arguments, apply, call und der Debugger

Es gibt ein paar Ei­gen­hei­ten in Ja­va­Script, die in an­de­ren Pro­gram­mier­spra­chen so nicht vor­kom­men. Da­zu ge­hört das An­wen­den von Me­tho­den auf be­lie­bi­ge Ob­jek­te. Läuft mal et­was schief, kann das mit dem De­bug­ger un­ter­sucht werden.

Die folgende simple Datenbank enthält eine insert-Methode, die im Prototyp-Objekt definiert ist. Wird ihr ein einfaches Objekt als Parameter übergeben, prüft sie in einer if-Bedingung, ob der Wert auch wirklich ein Objekt ist, und pusht es dann in das datensatz-Array.

function Datenbank () {
  this.datensatz = [];
}

Datenbank.prototype = {
  insert: function (obj) {
    if (obj instanceof Object) {
      this.datensatz.push(obj);
    }
  }
};

Die Datenbank kann nun instanziiert und einer Variable zugewiesen werden und die insert-Methode daran aufgerufen werden.

var db = new Datenbank();
db.insert({ "mein": "Objekt" });
db.datensatz;
// => [{ mein: "Objekt" }]

Es kann nützlich sein, auch mehrere Objekte auf einmal der Methode übergeben zu können. Ist das möglich?

Zugriff auf Funktionsparameter per arguments

Innerhalb einer Funktion gibt es ein Objekt namens arguments, das eine Liste aller übergebenen Parameter ist.
Das folgende Beispiel ist eine Funktion, die das arguments-Objekt einfach nur zurückgibt.

function showArguments() {
  return arguments;
}

// Die Funktion wird mit verschiedenen
// Parametern aufgerufen
showArguments("Test", 42, true, {});
// => ["Test", 42, true, {}]

Die Funktion gibt die Liste an übergebenen Parametern zurück. Obwohl es eine Liste ist und möglicherweise so aussieht wie ein Array, ist es wichtig, darauf hinzuweisen, dass es keines ist.

var args = showArguments("Argumente", "der", "Funktion");

// Wie Arrays hat arguments ein length-Property
args.length
// => 3

// Wie bei Arrays könnt ihr per Klammer-
// Notation auf einzelne Werte zugreifen
args[1]
// => "der"

// Aber: Es ist kein Array!
args instanceof Array
// => false

// Deswegen gibt es auch keine Array-Methoden
args.slice();
// => Uncaught TypeError: args.slice is not a function

Mit dem Argumente-Objekt kann die insert-Methode so umgeschrieben werden, dass sie beliebig viele Objekte annehmen und dem Datensatz hinzufügen kann. Obwohl es kein Array ist, hat es eine length-Property und einzelne Elemente können per Index angesprochen werden. Deswegen ist eine for-Schleife geeignet, um die Parameter abzugehen.

Datenbank.prototype.insert = function () {
  for (var i = 0, obj; i < arguments.length; i++) {
    obj = arguments[i];
    if (obj instanceof Object) {
      this.datensatz.push(obj);
    }
  }
};

db = new Datenbank();
db.insert(
  { beliebig: "viele" },
  { Objekte: "können nun" },
  { hinzugefügt: "werden" }
);

db.datensatz
// => [{ beliebig: "viele" }, { Objekte: "können nun" }, { hinzugefügt: "werden" }]

Um Wege noch mehr abzukürzen, wäre es praktisch, Werte für die Datenbank schon dem Konstruktor übergeben zu können. Dafür muss einfach nur die insert-Methode schon im Konstruktor aufgerufen werden.

function Datenbank () {
  this.datensatz = [];
  // Durch den Aufruf im Konstruktor kann
  // der Inhalt des Datensatzes schon beim
  // Instanziieren übergeben werden:
  this.insert(arguments);
}
Datenbank.prototype = {
  insert: function () {
    for (var i = 0, obj; i < arguments.length; i++) {
      obj = arguments[i];
      if (obj instanceof Object) {
        this.datensatz.push(obj);
      }
    }
  }
};

// Drei Objekte werden übergeben
db = new Datenbank(
  { Objekte: "werden nun" },
  { schon: "beim" },
  { Instanziieren: "übergeben" }
);

// Moment, etwas stimmt nicht:
db.datensatz.length
// => 1

Der Konstruktor wurde mit drei Objekten aufgerufen, aber der Datensatz hat eine Länge von 1. Hier liegt offenbar ein Fehler vor. Was ist passiert?

Fehler im Code untersuchen mit dem Debugger

Es kommt während des Programmierens immer wieder vor, dass sich ein Programm oder eine Prozedur nicht so verhält, wie sie angedacht war. Manchmal ist der Fehler bei näherem Hinsehen offensichtlich, manchmal kann die Fehlersuche aber auch sehr mühsam sein und sich in die Länge ziehen.
Um die Suche etwas zu erleichtern, gibt es in vielen Programmier­umgebungen ein Werkzeug namens Debugger. Ein Bug ist ein Programmfehler. Die wörtliche Übersetzung ist Käfer.
Früher waren Computer und Prozessoren so groß, dass Käfer in sie hinein­krabbeln und durcheinander bringen konnten. Die Entwickler mussten dann den Käfer im Computer suchen, um das Problem zu beheben. Das hat sich auf die Softwareentwicklung übertragen und looking for a bug (einen Fehler suchen), finding a bug (einen Fehler finden), debugging (Fehler suchen und beseitigen) sind heutzutage gängige Begrifflichkeiten, die allesamt keine Insekten meinen, sondern Software-Anomalien.

Heutzutage enthalten die gängigsten Browser einen Debugger, ein Werkzeug, mit dem JavaScript-Code Anweisung für Anweisung durchgegangen wird, um einen Fehler zu finden. Vom Code aus kann dieser mit dem debugger;-Statement aufgerufen werden.

Öffnet eure Konsole, um den Debugger in eurem Browser auszuprobieren.

// Code, der nach dem debugger-Statement
// folgt, kann im Debugger Anweisung für
// Anweisung manuell durchschritten werden
debugger; db = new Datenbank(
  { Objekte: "werden nun" },
  { schon: "beim" },
  { Instanziieren: "übergeben" }
);

Ansicht des Debuggers in Chrome

Über die Kontrollelemente des Debuggers werden die Anweisungen nacheinander durchgangen. Liegt der Cursor über einzelnen Werten, klappt sich ein Overlay aus, mit dem der entsprechende Wert untersucht werden kann.

Durch den Tooltip des arguments-Wert wird ersichtlich, dass alle Parameter vom Konstruktor in Form eines einzelnen Argumente-Objekts der insert-Methode übergeben wurden

Durch die Analyse mit dem Debugger wird der Fehler ersichtlich: Die drei Parameter, mit denen der Konstruktor aufgerufen wird, werden in Form eines einzelnen Argumente-Objekts der insert-Methode übergeben, die dieses Objekt dem datensatz-Array hinzufügt. Deswegen ist nur ein Objekt im datensatz gewesen. Doch wie lässt sich das Problem lösen?

Aufruf von Funktionen über call und apply

Funktionen in JavaScript sind zum einen ein eigener Datentyp, zum anderen sind sie aber auch Objekte.

var fn = function () {};

// Funktionen sind ein eigener Datentyp
typeof fn
// => "function"

// Und gleichzeitig auch Objekte
fn instanceof Object
// => true

Deswegen haben Funktionen auch eigene Properties wie zum Beispiel das prototype-Property.
Sie haben auch eigene Methoden, darunter call und apply. Mit diesen Methoden lässt sich der Kontext einer Funktion setzen. Was ist der Kontext, fragt ihr euch? Der Kontext in JavaScript ist das Objekt, das in einer Funktion per this referenziert wird.
Über call oder apply kann als erster Parameter ein Objekt übergeben werden, auf das eine Funktion angewendet werden soll.

function test() {
  return this;
}

test(); // Gibt globales Objekt zurück
// => window

// Plötzlich ist this etwas anderes:
test.call(new Array());
// => []

Auf diese Weise lassen sich auch Methoden, also Funktionen, die zu einem Objekt gehören, auf völlig andere, beliebige Objekte anwenden.

var andereDb = { datensatz: [] };

// Der erste Parameter von call ist das
// Objekt, auf das die Funktion angewendet
// werden soll. Alle weiteren Parameter
// werden der Funktion unverändert
// weitergereicht.
db.insert.call(andereDb, { ein: "Objekt" });

// Nun hat die insert-Methode von db ein
// Objekt in andereDb.datensatz hinzugefügt
andereDb.datensatz
// => [{ ein: "Objekt" }]

Der Unterschied zwischen call und apply liegt darin, wie die Funktionsparameter übergeben werden:

  • call nimmt beliebig viele Parameter entgegen, wobei der erste Parameter der Kontext ist, in dem die Funktion ausgeführt werden soll, und alle weiteren die Parameter, mit denen die Funktion aufgerufen werden soll
    fn.call(obj, param1, param2, param3, …);
    
  • apply nimmt zwei Parameter entgegen, wobei der erste Parameter der Kontext ist, in dem die Funktion ausgeführt werden soll, und der zweite eine Liste aller Parameter, mit denen die Funktion aufgerufen werden soll
    fn.apply(obj, [param1, param2, param3, …]);
    

Deswegen ist apply gut, um Argumente von einer Funktion zur nächsten weiterzureichen. Das kann auch im Datenbank-Konstruktor genutzt werden.

function Datenbank () {
  this.datensatz = [];
  this.insert.apply(this, arguments);
}
// Wenn ihr den Konstruktor überschreibt,
// müsst ihr auch dessen Prototyp neu setzen
Datenbank.prototype = … // (siehe oben)

Die insert-Methode wird mit apply ausgeführt und ihr werden zwei Parameter übergeben:

  1. this, also der aktuelle Kontext der Funktion. Das bedeutet, dass der Kontext unverändert bleibt.
  2. arguments, also die Liste der Argumente. Hier liegt der Trick und der Grund, warum an dieser Stelle apply genutzt wird: Ohne apply wurde einfach nur das gesamte Argumente-Objekt der insert-Methode übergeben. Mit apply wird nicht das Argumente-Objekt, sondern die Argumente selbst übergeben. Dadurch fügt insert die Objekte dem datensatz-Array hinzu, die dem Konstruktor übergeben wurden.
db = new Datenbank(
  { Objekte: "werden nun" },
  { schon: "beim" },
  { Instanziieren: "übergeben" }
);

db.datensatz
// => [{ Objekte: "werden nun" }, { schon: "beim" }, { Instanziieren: "übergeben" }]