Zum Inhalt springen
aviral gupta

// I2.5 · ca. 45 Min. · Aufbau

Projekt: ein Lagermodell

Sie bauen eine Lagerseite, auf der zwei Klassen die Daten halten und jede Änderung prüfen, und die Tabelle wird nach jedem Hinzufügen oder Entfernen aus ihnen gezeichnet.

Lektion 5 von 5 in I2 Objekte, Prototypen und Klassen

Ende des Moduls

Danach können Sie

  • Die Daten mit Item und Inventory modellieren: private Felder, Getter für berechnete Werte und eine statische Factory fromJSON
  • Daten in constructors und Methoden durch Werfen prüfen und die Fehlermeldung auf der Seite anzeigen
  • Tabelle und Zusammenfassung nach jeder Änderung aus dem Modell darstellen
  1. Aufwärmen · Aufgabe 1 von 7

    Zum Aufwärmen, aus Lektion I2.4: Was gibt dieser Code aus?

    class Item {
      #price = 2;
      #quantity = 3;
      get value() {
        return this.#price * this.#quantity;
      }
    }
    const item = new Item();
    console.log(item.value, Object.keys(item).length);
  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie es vorher, bevor Sie weiterlesen: Drei Preise gehen in den constructor. Was gibt dieser Code aus?

    class Item {
      constructor(name, price) {
        if (!(price >= 0)) throw new RangeError(`invalid price: ${price}`);
        this.name = name;
        this.price = price;
      }
    }
    const made = [];
    for (const price of [2, -1, Number("abc")]) {
      try {
        made.push(new Item("pen", price).price);
      } catch (error) {
        made.push(error.message);
      }
    }
    console.log(made.join(" | "));
  3. Üben · Aufgabe 3 von 7

    Ergänzen Sie das Schlüsselwort, das einen bereits hinzugefügten Artikel ablehnt, sodass das catch des Aufrufers den Fehler sieht.

    class Inventory {
      #items = [];
      add(item) {
        if (this.#items.includes(item)) ____ new Error("already added");
        this.#items.push(item);
      }
      get count() {
        return this.#items.length;
      }
    }
    if (this.#items.includes(item))new Error("already added");
  4. Üben · Aufgabe 4 von 7

    Der Add-Button wird angeklickt. Bringen Sie die Schritte der Seite in die richtige Reihenfolge.

    1. 1.Die Meldung zeigt „Added …“ oder die Meldung des Fehlers
    2. 2.render() baut die Zeilen und die Zusammenfassung aus dem Inventar neu auf
    3. 3.new Item(…) wird aus den Eingaben gebaut und prüft sie
    4. 4.Der submit-Listener ruft event.preventDefault() auf
    5. 5.inventory.add(item) prüft auf ein Duplikat und speichert den Artikel
  5. Üben · Aufgabe 5 von 7

    Eine statische Factory liest JSON. Was gibt dieser Code aus?

    class Item {
      constructor(name, price) {
        this.name = name;
        this.price = price;
      }
    }
    class Inventory {
      #items = [];
      static fromJSON(text) {
        const inventory = new Inventory();
        for (const {name, price} of JSON.parse(text)) inventory.add(new Item(name, price));
        return inventory;
      }
      add(item) {
        this.#items.push(item);
      }
      get total() {
        return this.#items.reduce((sum, item) => sum + item.price, 0);
      }
    }
    const inv = Inventory.fromJSON('[{"name": "pen", "price": 2}, {"name": "cup", "price": 5}]');
    console.log(inv.total, inv instanceof Inventory);
  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. Der Getter gibt das private Array selbst zurück. Was gibt dieser Code aus?

    class Inventory {
      #items = ["pen"];
      get items() {
        return this.#items;
      }
      get count() {
        return this.#items.length;
      }
    }
    const inv = new Inventory();
    inv.items.push("hacked");
    console.log(inv.count);
  7. Anwenden · Aufgabe 7 von 7

    Kleine Aufgabe: Erweitern Sie Ihr Lager. Ergänzen Sie ein statisches Feld LOW = 5 und einen Getter lowStock in Inventory, der die Artikel mit einer Menge unter Inventory.LOW zurückgibt. Fügen Sie unter der Zusammenfassung einen Absatz hinzu und füllen Sie ihn in render(): "Low stock: Stapler" oder "Low stock: none". Probieren Sie es über einen lokalen Webserver aus.

    Prüfen Sie Ihr Ergebnis anhand dieser Liste

Selbst programmieren

Lesen Sie das ausgearbeitete Beispiel und lösen Sie dann die Übungen. Ihr Code läuft in Ihrem Browser oder auf Ihrem Computer und wird nie hochgeladen.

Ausgearbeitetes Beispiel

Das Lager, von Anfang bis Ende

Die Seite hat ein Formular, eine Meldungszeile, eine Tabelle mit leerem Körper und eine Zusammenfassung. main.js enthält die beiden Klassen, baut das Inventar mit Inventory.fromJSON aus JSON auf und stellt es dar. Die Listener für submit und click ändern das Modell und stellen erneut dar. Am Ende benutzt das Skript seine eigene Seite, so wie es die Tests der Übungen tun: Es fügt einen guten und drei schlechte Artikel hinzu und entfernt dann den Stapler.

main.js

class Item {
  #name;
  #price;
  #quantity;

  constructor(name, price, quantity) {
    if (typeof name !== 'string' || name.trim() === '') throw new TypeError('name must not be empty');
    if (!(price >= 0)) throw new RangeError(`invalid price: ${price}`);
    if (!Number.isInteger(quantity) || quantity < 0) throw new RangeError(`invalid quantity: ${quantity}`);
    this.#name = name.trim();
    this.#price = price;
    this.#quantity = quantity;
  }

  get name() {
    return this.#name;
  }

  get price() {
    return this.#price;
  }

  get quantity() {
    return this.#quantity;
  }

  get value() {
    return this.#price * this.#quantity;
  }
}

class Inventory {
  #items = [];

  static fromJSON(text) {
    const inventory = new Inventory();
    for (const { name, price, quantity } of JSON.parse(text)) {
      inventory.add(new Item(name, price, quantity));
    }
    return inventory;
  }

  add(item) {
    if (!(item instanceof Item)) throw new TypeError('only an Item can be added');
    if (this.#items.some((other) => other.name === item.name)) {
      throw new Error(`${item.name} is already in the inventory`);
    }
    this.#items.push(item);
  }

  remove(name) {
    const index = this.#items.findIndex((item) => item.name === name);
    if (index === -1) throw new Error(`${name} is not in the inventory`);
    this.#items.splice(index, 1);
  }

  get items() {
    return [...this.#items];
  }

  get count() {
    return this.#items.length;
  }

  get totalValue() {
    return this.#items.reduce((sum, item) => sum + item.value, 0);
  }
}

const STOCK = '[{"name": "Pencil", "price": 0.5, "quantity": 40}, {"name": "Notebook", "price": 2.25, "quantity": 12}, {"name": "Stapler", "price": 7.9, "quantity": 3}]';
const inventory = Inventory.fromJSON(STOCK);

const form = document.querySelector('#add-form');
const nameInput = document.querySelector('#item-name');
const priceInput = document.querySelector('#item-price');
const quantityInput = document.querySelector('#item-quantity');
const rows = document.querySelector('#rows');
const message = document.querySelector('#message');
const summary = document.querySelector('#summary');

const euro = (amount) => `${amount.toFixed(2)} EUR`;

function cell(content) {
  const td = document.createElement('td');
  td.append(content);
  return td;
}

// The table is always rebuilt from the model.
function render() {
  rows.replaceChildren(
    ...inventory.items.map((item) => {
      const remove = document.createElement('button');
      remove.type = 'button';
      remove.textContent = 'Remove';
      remove.dataset.name = item.name;
      const row = document.createElement('tr');
      row.append(cell(item.name), cell(euro(item.price)), cell(String(item.quantity)), cell(euro(item.value)), cell(remove));
      return row;
    })
  );
  summary.textContent = `${inventory.count} items, total value ${euro(inventory.totalValue)}`;
}

form.addEventListener('submit', (event) => {
  event.preventDefault();
  try {
    const item = new Item(nameInput.value, Number(priceInput.value), Number(quantityInput.value));
    inventory.add(item);
    message.textContent = `Added ${item.name}.`;
    form.reset();
  } catch (error) {
    message.textContent = error.message;
  }
  render();
});

rows.addEventListener('click', (event) => {
  const button = event.target.closest('button');
  if (!button) return;
  try {
    inventory.remove(button.dataset.name);
    message.textContent = `Removed ${button.dataset.name}.`;
  } catch (error) {
    message.textContent = error.message;
  }
  render();
});

render();

// The script tries its own page, the way the exercise tests do.
function submit(name, price, quantity) {
  nameInput.value = name;
  priceInput.value = price;
  quantityInput.value = quantity;
  form.querySelector('button').click();
  console.log(message.textContent);
}
console.log(summary.textContent);
submit('Ruler', '1.2', '10');
submit('Pencil', '0.5', '5');
submit('Eraser', '-1', '5');
submit('Glue', '1.5', '2.5');
rows.querySelector('[data-name="Stapler"]').click();
console.log(message.textContent);
console.log([...rows.querySelectorAll('tr')].map((row) => row.firstChild.textContent).join(', '));

index.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Inventory</title>
</head>
<body>
  <h1>Inventory</h1>
  <form id="add-form" novalidate>
    <label>Name <input id="item-name"></label>
    <label>Price <input id="item-price" type="number" step="0.01"></label>
    <label>Quantity <input id="item-quantity" type="number"></label>
    <button type="submit">Add</button>
  </form>
  <p id="message"></p>
  <table>
    <thead>
      <tr><th>Item</th><th>Price</th><th>Quantity</th><th>Value</th><th></th></tr>
    </thead>
    <tbody id="rows"></tbody>
  </table>
  <p id="summary"></p>
  <script type="module" src="main.js"></script>
</body>
</html>

Um die Seite auf Ihrem Computer auszuführen, öffnen Sie index.html über einen lokalen Webserver, nicht als Datei: Browser führen Modul-Skripte von file://-Adressen nicht aus. Für die Prüfungen installieren Sie zuerst jsdom (die Befehle unten).

Ausgabe

3 items, total value 70.70 EUR
Added Ruler.
Pencil is already in the inventory
invalid price: -1
invalid quantity: 2.5
Removed Stapler.
Pencil, Notebook, Ruler
<!-- #summary -->
<p id="summary">3 items, total value 59.00 EUR</p>
  • Die drei abgelehnten Artikel erreichten die Tabelle nie: Jeder constructor oder jedes add warf, und die Meldung zeigt den Grund.
  • Der Gesamtwert wird bei jeder Darstellung aus den Artikeln berechnet, nie gespeichert.
  • Nach Remove ist der Stapler aus dem Modell verschwunden, render() zeichnete also nur drei Zeilen.
  • toFixed(2) machte aus 7.9 × 3, in Gleitkommazahlen 23.700000000000003, den Wert 23.70.
Ändern und ausführen

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser den JavaScript-Runner herunter (bis zu 0.1 MB) und speichert ihn im Cache. Ihr Code läuft in der eigenen Engine Ihres Browsers und bleibt auf Ihrem Gerät.

Ihre Seite

Ihre Seite läuft in einem abgeschotteten Rahmen. Sie hat keinen Zugriff auf diese Website, und ihr localStorage liegt nur für diesen Lauf im Speicher: Der nächste Lauf beginnt leer.

Führen Sie das Programm aus, um seine Seite hier zu sehen.

Übungen

Übung 1 von 3

Artikel, die schlechte Daten ablehnen

Vervollständigen Sie den constructor von Item. Er muss einen TypeError mit der Meldung name must not be empty werfen, wenn der Name leer ist oder nur aus Leerzeichen besteht, einen RangeError invalid price: -1 für einen Preis, der nicht >= 0 ist, und einen RangeError invalid quantity: 2.5 für eine Menge, die keine ganze Zahl >= 0 ist. Die Seite zeigt die Meldung jedes Fehlers bereits an. Der Starter prüft nichts.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser den JavaScript-Runner herunter (bis zu 0.1 MB) und speichert ihn im Cache. Ihr Code läuft in der eigenen Engine Ihres Browsers und bleibt auf Ihrem Gerät.

Ihre Seite

Ihre Seite läuft in einem abgeschotteten Rahmen. Sie hat keinen Zugriff auf diese Website, und ihr localStorage liegt nur für diesen Lauf im Speicher: Der nächste Lauf beginnt leer.

Führen Sie das Programm aus, um seine Seite hier zu sehen.

Hinweise
  1. Hinweis 1

    Führen Sie die Tests aus: Die schlechten Artikel werden hinzugefügt. Wo sollte ein Item seine Daten ablehnen, bevor es etwas speichert?

  2. Hinweis 2

    Drei if-Anweisungen am Anfang des constructors, jede mit throw. Für den Namen: typeof name !== 'string' || name.trim() === ''.

  3. Hinweis 3

    !(price >= 0) lehnt auch NaN ab. Für die Menge: !Number.isInteger(quantity) || quantity < 0.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

class Item {
  #name;
  #price;
  #quantity;

  constructor(name, price, quantity) {
    if (typeof name !== 'string' || name.trim() === '') throw new TypeError('name must not be empty');
    if (!(price >= 0)) throw new RangeError(`invalid price: ${price}`);
    if (!Number.isInteger(quantity) || quantity < 0) throw new RangeError(`invalid quantity: ${quantity}`);
    this.#name = name.trim();
    this.#price = price;
    this.#quantity = quantity;
  }

  get name() {
    return this.#name;
  }

  get price() {
    return this.#price;
  }

  get quantity() {
    return this.#quantity;
  }

  get value() {
    return this.#price * this.#quantity;
  }
}

class Inventory {
  #items = [];

  static fromJSON(text) {
    const inventory = new Inventory();
    for (const { name, price, quantity } of JSON.parse(text)) {
      inventory.add(new Item(name, price, quantity));
    }
    return inventory;
  }

  add(item) {
    if (!(item instanceof Item)) throw new TypeError('only an Item can be added');
    if (this.#items.some((other) => other.name === item.name)) {
      throw new Error(`${item.name} is already in the inventory`);
    }
    this.#items.push(item);
  }

  remove(name) {
    const index = this.#items.findIndex((item) => item.name === name);
    if (index === -1) throw new Error(`${name} is not in the inventory`);
    this.#items.splice(index, 1);
  }

  get items() {
    return [...this.#items];
  }

  get count() {
    return this.#items.length;
  }

  get totalValue() {
    return this.#items.reduce((sum, item) => sum + item.value, 0);
  }
}

const STOCK = '[{"name": "Pencil", "price": 0.5, "quantity": 40}, {"name": "Notebook", "price": 2.25, "quantity": 12}, {"name": "Stapler", "price": 7.9, "quantity": 3}]';
const inventory = Inventory.fromJSON(STOCK);

const form = document.querySelector('#add-form');
const nameInput = document.querySelector('#item-name');
const priceInput = document.querySelector('#item-price');
const quantityInput = document.querySelector('#item-quantity');
const rows = document.querySelector('#rows');
const message = document.querySelector('#message');
const summary = document.querySelector('#summary');

const euro = (amount) => `${amount.toFixed(2)} EUR`;

function cell(content) {
  const td = document.createElement('td');
  td.append(content);
  return td;
}

// The table is always rebuilt from the model.
function render() {
  rows.replaceChildren(
    ...inventory.items.map((item) => {
      const remove = document.createElement('button');
      remove.type = 'button';
      remove.textContent = 'Remove';
      remove.dataset.name = item.name;
      const row = document.createElement('tr');
      row.append(cell(item.name), cell(euro(item.price)), cell(String(item.quantity)), cell(euro(item.value)), cell(remove));
      return row;
    })
  );
  summary.textContent = `${inventory.count} items, total value ${euro(inventory.totalValue)}`;
}

form.addEventListener('submit', (event) => {
  event.preventDefault();
  try {
    const item = new Item(nameInput.value, Number(priceInput.value), Number(quantityInput.value));
    inventory.add(item);
    message.textContent = `Added ${item.name}.`;
    form.reset();
  } catch (error) {
    message.textContent = error.message;
  }
  render();
});

rows.addEventListener('click', (event) => {
  const button = event.target.closest('button');
  if (!button) return;
  try {
    inventory.remove(button.dataset.name);
    message.textContent = `Removed ${button.dataset.name}.`;
  } catch (error) {
    message.textContent = error.message;
  }
  render();
});

render();
Auf dem eigenen Computer ausführen

Installieren Sie ECMAScript 2026 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.js

class Item {
  #name;
  #price;
  #quantity;

  constructor(name, price, quantity) {
    // TODO: throw a TypeError for an empty name, and a RangeError for a price
    // that is not >= 0 or a quantity that is not a whole number >= 0.
    this.#name = name.trim();
    this.#price = price;
    this.#quantity = quantity;
  }

  get name() {
    return this.#name;
  }

  get price() {
    return this.#price;
  }

  get quantity() {
    return this.#quantity;
  }

  get value() {
    return this.#price * this.#quantity;
  }
}

class Inventory {
  #items = [];

  static fromJSON(text) {
    const inventory = new Inventory();
    for (const { name, price, quantity } of JSON.parse(text)) {
      inventory.add(new Item(name, price, quantity));
    }
    return inventory;
  }

  add(item) {
    if (!(item instanceof Item)) throw new TypeError('only an Item can be added');
    if (this.#items.some((other) => other.name === item.name)) {
      throw new Error(`${item.name} is already in the inventory`);
    }
    this.#items.push(item);
  }

  remove(name) {
    const index = this.#items.findIndex((item) => item.name === name);
    if (index === -1) throw new Error(`${name} is not in the inventory`);
    this.#items.splice(index, 1);
  }

  get items() {
    return [...this.#items];
  }

  get count() {
    return this.#items.length;
  }

  get totalValue() {
    return this.#items.reduce((sum, item) => sum + item.value, 0);
  }
}

const STOCK = '[{"name": "Pencil", "price": 0.5, "quantity": 40}, {"name": "Notebook", "price": 2.25, "quantity": 12}, {"name": "Stapler", "price": 7.9, "quantity": 3}]';
const inventory = Inventory.fromJSON(STOCK);

const form = document.querySelector('#add-form');
const nameInput = document.querySelector('#item-name');
const priceInput = document.querySelector('#item-price');
const quantityInput = document.querySelector('#item-quantity');
const rows = document.querySelector('#rows');
const message = document.querySelector('#message');
const summary = document.querySelector('#summary');

const euro = (amount) => `${amount.toFixed(2)} EUR`;

function cell(content) {
  const td = document.createElement('td');
  td.append(content);
  return td;
}

// The table is always rebuilt from the model.
function render() {
  rows.replaceChildren(
    ...inventory.items.map((item) => {
      const remove = document.createElement('button');
      remove.type = 'button';
      remove.textContent = 'Remove';
      remove.dataset.name = item.name;
      const row = document.createElement('tr');
      row.append(cell(item.name), cell(euro(item.price)), cell(String(item.quantity)), cell(euro(item.value)), cell(remove));
      return row;
    })
  );
  summary.textContent = `${inventory.count} items, total value ${euro(inventory.totalValue)}`;
}

form.addEventListener('submit', (event) => {
  event.preventDefault();
  try {
    const item = new Item(nameInput.value, Number(priceInput.value), Number(quantityInput.value));
    inventory.add(item);
    message.textContent = `Added ${item.name}.`;
    form.reset();
  } catch (error) {
    message.textContent = error.message;
  }
  render();
});

rows.addEventListener('click', (event) => {
  const button = event.target.closest('button');
  if (!button) return;
  try {
    inventory.remove(button.dataset.name);
    message.textContent = `Removed ${button.dataset.name}.`;
  } catch (error) {
    message.textContent = error.message;
  }
  render();
});

render();

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {loadPage} from './learnrun.js';

const text = (page, selector) => page.document.querySelector(selector).textContent;
const names = (page) => [...page.document.querySelectorAll('#rows tr')].map((row) => row.firstChild.textContent);
function submit(page, name, price, quantity) {
  page.document.querySelector('#item-name').value = name;
  page.document.querySelector('#item-price').value = price;
  page.document.querySelector('#item-quantity').value = quantity;
  page.document.querySelector('#add-form button').click();
}
function removeButton(page, name) {
  const button = page.document.querySelector(`#rows button[data-name="${name}"]`);
  assert.ok(button, `es gibt keinen Remove-Button für ${name}; die Zeilen sind ${JSON.stringify(names(page))}`);
  return button;
}

test('Ein gültiger Artikel wird als neue Zeile hinzugefügt', async () => {
  const page = await loadPage();
  submit(page, 'Ruler', '1.2', '10');
  assert.equal(text(page, '#message'), 'Added Ruler.', 'die Meldung lautet ' + text(page, '#message'));
  assert.deepEqual(names(page), ['Pencil', 'Notebook', 'Stapler', 'Ruler'], 'die Zeilen sind ' + JSON.stringify(names(page)));
});

test('Ein negativer Preis wird abgelehnt', async () => {
  const page = await loadPage();
  submit(page, 'Eraser', '-1', '5');
  assert.equal(text(page, '#message'), 'invalid price: -1', 'die Meldung lautet ' + text(page, '#message'));
  assert.equal(names(page).length, 3, 'die Zeilen sind ' + JSON.stringify(names(page)));
});

test('Eine Menge von 2.5 wird abgelehnt', async () => {
  const page = await loadPage();
  submit(page, 'Glue', '1.5', '2.5');
  assert.equal(text(page, '#message'), 'invalid quantity: 2.5', 'die Meldung lautet ' + text(page, '#message'));
});

test('Ein Name nur aus Leerzeichen wird abgelehnt', async () => {
  const page = await loadPage();
  submit(page, '   ', '1', '1');
  assert.equal(text(page, '#message'), 'name must not be empty', 'die Meldung lautet ' + text(page, '#message'));
  assert.equal(names(page).length, 3, 'die Zeilen sind ' + JSON.stringify(names(page)));
});

index.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Inventory</title>
</head>
<body>
  <h1>Inventory</h1>
  <form id="add-form" novalidate>
    <label>Name <input id="item-name"></label>
    <label>Price <input id="item-price" type="number" step="0.01"></label>
    <label>Quantity <input id="item-quantity" type="number"></label>
    <button type="submit">Add</button>
  </form>
  <p id="message"></p>
  <table>
    <thead>
      <tr><th>Item</th><th>Price</th><th>Quantity</th><th>Value</th><th></th></tr>
    </thead>
    <tbody id="rows"></tbody>
  </table>
  <p id="summary"></p>
  <script type="module" src="main.js"></script>
</body>
</html>

package.json

{
  "type": "module"
}

package.json sagt Node.js, dass die .js-Dateien Module sind; lassen Sie die Datei im Ordner.

Programm ausführen:

Um die Seite auf Ihrem Computer auszuführen, öffnen Sie index.html über einen lokalen Webserver, nicht als Datei: Browser führen Modul-Skripte von file://-Adressen nicht aus. Für die Prüfungen installieren Sie zuerst jsdom (die Befehle unten).

Prüfungen ausführen (learnrun.js muss im selben Ordner liegen):

npm install jsdom@28.1.0
node --test
learnrun.js herunterladen

Übung 2 von 3

Einen Artikel entfernen

Vervollständigen Sie remove(name) in Inventory. Suchen Sie den Artikel mit diesem Namen im privaten Array #items und nehmen Sie ihn mit splice heraus; gibt es keinen, werfen Sie einen Error mit der Meldung <name> is not in the inventory. Die Remove-Buttons rufen es bereits auf und stellen danach neu dar. Das remove im Starter tut nichts, die Zeilen bleiben also stehen.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser den JavaScript-Runner herunter (bis zu 0.1 MB) und speichert ihn im Cache. Ihr Code läuft in der eigenen Engine Ihres Browsers und bleibt auf Ihrem Gerät.

Ihre Seite

Ihre Seite läuft in einem abgeschotteten Rahmen. Sie hat keinen Zugriff auf diese Website, und ihr localStorage liegt nur für diesen Lauf im Speicher: Der nächste Lauf beginnt leer.

Führen Sie das Programm aus, um seine Seite hier zu sehen.

Hinweise
  1. Hinweis 1

    Führen Sie die Tests aus: Die Zeile Notebook ist noch da. Was macht remove() mit dem privaten Array?

  2. Hinweis 2

    findIndex((item) => item.name === name) liefert die Position oder -1, wenn es keinen solchen Artikel gibt.

  3. Hinweis 3

    if (index === -1) throw new Error(`${name} is not in the inventory`); this.#items.splice(index, 1);

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

class Item {
  #name;
  #price;
  #quantity;

  constructor(name, price, quantity) {
    if (typeof name !== 'string' || name.trim() === '') throw new TypeError('name must not be empty');
    if (!(price >= 0)) throw new RangeError(`invalid price: ${price}`);
    if (!Number.isInteger(quantity) || quantity < 0) throw new RangeError(`invalid quantity: ${quantity}`);
    this.#name = name.trim();
    this.#price = price;
    this.#quantity = quantity;
  }

  get name() {
    return this.#name;
  }

  get price() {
    return this.#price;
  }

  get quantity() {
    return this.#quantity;
  }

  get value() {
    return this.#price * this.#quantity;
  }
}

class Inventory {
  #items = [];

  static fromJSON(text) {
    const inventory = new Inventory();
    for (const { name, price, quantity } of JSON.parse(text)) {
      inventory.add(new Item(name, price, quantity));
    }
    return inventory;
  }

  add(item) {
    if (!(item instanceof Item)) throw new TypeError('only an Item can be added');
    if (this.#items.some((other) => other.name === item.name)) {
      throw new Error(`${item.name} is already in the inventory`);
    }
    this.#items.push(item);
  }

  remove(name) {
    const index = this.#items.findIndex((item) => item.name === name);
    if (index === -1) throw new Error(`${name} is not in the inventory`);
    this.#items.splice(index, 1);
  }

  get items() {
    return [...this.#items];
  }

  get count() {
    return this.#items.length;
  }

  get totalValue() {
    return this.#items.reduce((sum, item) => sum + item.value, 0);
  }
}

const STOCK = '[{"name": "Pencil", "price": 0.5, "quantity": 40}, {"name": "Notebook", "price": 2.25, "quantity": 12}, {"name": "Stapler", "price": 7.9, "quantity": 3}]';
const inventory = Inventory.fromJSON(STOCK);

const form = document.querySelector('#add-form');
const nameInput = document.querySelector('#item-name');
const priceInput = document.querySelector('#item-price');
const quantityInput = document.querySelector('#item-quantity');
const rows = document.querySelector('#rows');
const message = document.querySelector('#message');
const summary = document.querySelector('#summary');

const euro = (amount) => `${amount.toFixed(2)} EUR`;

function cell(content) {
  const td = document.createElement('td');
  td.append(content);
  return td;
}

// The table is always rebuilt from the model.
function render() {
  rows.replaceChildren(
    ...inventory.items.map((item) => {
      const remove = document.createElement('button');
      remove.type = 'button';
      remove.textContent = 'Remove';
      remove.dataset.name = item.name;
      const row = document.createElement('tr');
      row.append(cell(item.name), cell(euro(item.price)), cell(String(item.quantity)), cell(euro(item.value)), cell(remove));
      return row;
    })
  );
  summary.textContent = `${inventory.count} items, total value ${euro(inventory.totalValue)}`;
}

form.addEventListener('submit', (event) => {
  event.preventDefault();
  try {
    const item = new Item(nameInput.value, Number(priceInput.value), Number(quantityInput.value));
    inventory.add(item);
    message.textContent = `Added ${item.name}.`;
    form.reset();
  } catch (error) {
    message.textContent = error.message;
  }
  render();
});

rows.addEventListener('click', (event) => {
  const button = event.target.closest('button');
  if (!button) return;
  try {
    inventory.remove(button.dataset.name);
    message.textContent = `Removed ${button.dataset.name}.`;
  } catch (error) {
    message.textContent = error.message;
  }
  render();
});

render();
Auf dem eigenen Computer ausführen

Installieren Sie ECMAScript 2026 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.js

class Item {
  #name;
  #price;
  #quantity;

  constructor(name, price, quantity) {
    if (typeof name !== 'string' || name.trim() === '') throw new TypeError('name must not be empty');
    if (!(price >= 0)) throw new RangeError(`invalid price: ${price}`);
    if (!Number.isInteger(quantity) || quantity < 0) throw new RangeError(`invalid quantity: ${quantity}`);
    this.#name = name.trim();
    this.#price = price;
    this.#quantity = quantity;
  }

  get name() {
    return this.#name;
  }

  get price() {
    return this.#price;
  }

  get quantity() {
    return this.#quantity;
  }

  get value() {
    return this.#price * this.#quantity;
  }
}

class Inventory {
  #items = [];

  static fromJSON(text) {
    const inventory = new Inventory();
    for (const { name, price, quantity } of JSON.parse(text)) {
      inventory.add(new Item(name, price, quantity));
    }
    return inventory;
  }

  add(item) {
    if (!(item instanceof Item)) throw new TypeError('only an Item can be added');
    if (this.#items.some((other) => other.name === item.name)) {
      throw new Error(`${item.name} is already in the inventory`);
    }
    this.#items.push(item);
  }

  remove(name) {
    // TODO: find the item by its name and take it out; throw an Error if there is none.
  }

  get items() {
    return [...this.#items];
  }

  get count() {
    return this.#items.length;
  }

  get totalValue() {
    return this.#items.reduce((sum, item) => sum + item.value, 0);
  }
}

const STOCK = '[{"name": "Pencil", "price": 0.5, "quantity": 40}, {"name": "Notebook", "price": 2.25, "quantity": 12}, {"name": "Stapler", "price": 7.9, "quantity": 3}]';
const inventory = Inventory.fromJSON(STOCK);

const form = document.querySelector('#add-form');
const nameInput = document.querySelector('#item-name');
const priceInput = document.querySelector('#item-price');
const quantityInput = document.querySelector('#item-quantity');
const rows = document.querySelector('#rows');
const message = document.querySelector('#message');
const summary = document.querySelector('#summary');

const euro = (amount) => `${amount.toFixed(2)} EUR`;

function cell(content) {
  const td = document.createElement('td');
  td.append(content);
  return td;
}

// The table is always rebuilt from the model.
function render() {
  rows.replaceChildren(
    ...inventory.items.map((item) => {
      const remove = document.createElement('button');
      remove.type = 'button';
      remove.textContent = 'Remove';
      remove.dataset.name = item.name;
      const row = document.createElement('tr');
      row.append(cell(item.name), cell(euro(item.price)), cell(String(item.quantity)), cell(euro(item.value)), cell(remove));
      return row;
    })
  );
  summary.textContent = `${inventory.count} items, total value ${euro(inventory.totalValue)}`;
}

form.addEventListener('submit', (event) => {
  event.preventDefault();
  try {
    const item = new Item(nameInput.value, Number(priceInput.value), Number(quantityInput.value));
    inventory.add(item);
    message.textContent = `Added ${item.name}.`;
    form.reset();
  } catch (error) {
    message.textContent = error.message;
  }
  render();
});

rows.addEventListener('click', (event) => {
  const button = event.target.closest('button');
  if (!button) return;
  try {
    inventory.remove(button.dataset.name);
    message.textContent = `Removed ${button.dataset.name}.`;
  } catch (error) {
    message.textContent = error.message;
  }
  render();
});

render();

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {loadPage} from './learnrun.js';

const text = (page, selector) => page.document.querySelector(selector).textContent;
const names = (page) => [...page.document.querySelectorAll('#rows tr')].map((row) => row.firstChild.textContent);
function submit(page, name, price, quantity) {
  page.document.querySelector('#item-name').value = name;
  page.document.querySelector('#item-price').value = price;
  page.document.querySelector('#item-quantity').value = quantity;
  page.document.querySelector('#add-form button').click();
}
function removeButton(page, name) {
  const button = page.document.querySelector(`#rows button[data-name="${name}"]`);
  assert.ok(button, `es gibt keinen Remove-Button für ${name}; die Zeilen sind ${JSON.stringify(names(page))}`);
  return button;
}

test('Remove nimmt die Zeile weg', async () => {
  const page = await loadPage();
  removeButton(page, 'Notebook').click();
  assert.deepEqual(names(page), ['Pencil', 'Stapler'], 'die Zeilen sind ' + JSON.stringify(names(page)));
  assert.equal(text(page, '#message'), 'Removed Notebook.', 'die Meldung lautet ' + text(page, '#message'));
});

test('Die Zusammenfassung folgt dem Entfernen', async () => {
  const page = await loadPage();
  removeButton(page, 'Notebook').click();
  assert.equal(text(page, '#summary'), '2 items, total value 43.70 EUR', 'die Zusammenfassung lautet ' + text(page, '#summary'));
});

test('Ein entfernter Artikel lässt sich erneut hinzufügen', async () => {
  const page = await loadPage();
  removeButton(page, 'Pencil').click();
  submit(page, 'Pencil', '0.6', '10');
  assert.equal(text(page, '#message'), 'Added Pencil.', 'die Meldung lautet ' + text(page, '#message'));
});

index.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Inventory</title>
</head>
<body>
  <h1>Inventory</h1>
  <form id="add-form" novalidate>
    <label>Name <input id="item-name"></label>
    <label>Price <input id="item-price" type="number" step="0.01"></label>
    <label>Quantity <input id="item-quantity" type="number"></label>
    <button type="submit">Add</button>
  </form>
  <p id="message"></p>
  <table>
    <thead>
      <tr><th>Item</th><th>Price</th><th>Quantity</th><th>Value</th><th></th></tr>
    </thead>
    <tbody id="rows"></tbody>
  </table>
  <p id="summary"></p>
  <script type="module" src="main.js"></script>
</body>
</html>

package.json

{
  "type": "module"
}

package.json sagt Node.js, dass die .js-Dateien Module sind; lassen Sie die Datei im Ordner.

Programm ausführen:

Um die Seite auf Ihrem Computer auszuführen, öffnen Sie index.html über einen lokalen Webserver, nicht als Datei: Browser führen Modul-Skripte von file://-Adressen nicht aus. Für die Prüfungen installieren Sie zuerst jsdom (die Befehle unten).

Prüfungen ausführen (learnrun.js muss im selben Ordner liegen):

npm install jsdom@28.1.0
node --test
learnrun.js herunterladen

Übung 3 von 3

Den Bestand mit einer statischen Factory laden

Vervollständigen Sie die statische Factory Inventory.fromJSON(text). Die Seite ruft sie mit einem JSON-Text aus drei Artikeln auf. Sie muss den Text mit JSON.parse parsen und für jeden Eintrag ein neues Item(name, price, quantity) zum neuen Inventar hinzufügen, bevor sie es zurückgibt. Der Starter gibt ein leeres Inventar zurück, die Tabelle beginnt also leer.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser den JavaScript-Runner herunter (bis zu 0.1 MB) und speichert ihn im Cache. Ihr Code läuft in der eigenen Engine Ihres Browsers und bleibt auf Ihrem Gerät.

Ihre Seite

Ihre Seite läuft in einem abgeschotteten Rahmen. Sie hat keinen Zugriff auf diese Website, und ihr localStorage liegt nur für diesen Lauf im Speicher: Der nächste Lauf beginnt leer.

Führen Sie das Programm aus, um seine Seite hier zu sehen.

Hinweise
  1. Hinweis 1

    Führen Sie die Tests aus: Die Tabelle ist leer. Was legt das fromJSON des Starters ins Inventar?

  2. Hinweis 2

    JSON.parse(text) liefert ein Array einfacher Objekte mit name, price und quantity.

  3. Hinweis 3

    for (const { name, price, quantity } of JSON.parse(text)) inventory.add(new Item(name, price, quantity));

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

class Item {
  #name;
  #price;
  #quantity;

  constructor(name, price, quantity) {
    if (typeof name !== 'string' || name.trim() === '') throw new TypeError('name must not be empty');
    if (!(price >= 0)) throw new RangeError(`invalid price: ${price}`);
    if (!Number.isInteger(quantity) || quantity < 0) throw new RangeError(`invalid quantity: ${quantity}`);
    this.#name = name.trim();
    this.#price = price;
    this.#quantity = quantity;
  }

  get name() {
    return this.#name;
  }

  get price() {
    return this.#price;
  }

  get quantity() {
    return this.#quantity;
  }

  get value() {
    return this.#price * this.#quantity;
  }
}

class Inventory {
  #items = [];

  static fromJSON(text) {
    const inventory = new Inventory();
    for (const { name, price, quantity } of JSON.parse(text)) {
      inventory.add(new Item(name, price, quantity));
    }
    return inventory;
  }

  add(item) {
    if (!(item instanceof Item)) throw new TypeError('only an Item can be added');
    if (this.#items.some((other) => other.name === item.name)) {
      throw new Error(`${item.name} is already in the inventory`);
    }
    this.#items.push(item);
  }

  remove(name) {
    const index = this.#items.findIndex((item) => item.name === name);
    if (index === -1) throw new Error(`${name} is not in the inventory`);
    this.#items.splice(index, 1);
  }

  get items() {
    return [...this.#items];
  }

  get count() {
    return this.#items.length;
  }

  get totalValue() {
    return this.#items.reduce((sum, item) => sum + item.value, 0);
  }
}

const STOCK = '[{"name": "Pencil", "price": 0.5, "quantity": 40}, {"name": "Notebook", "price": 2.25, "quantity": 12}, {"name": "Stapler", "price": 7.9, "quantity": 3}]';
const inventory = Inventory.fromJSON(STOCK);

const form = document.querySelector('#add-form');
const nameInput = document.querySelector('#item-name');
const priceInput = document.querySelector('#item-price');
const quantityInput = document.querySelector('#item-quantity');
const rows = document.querySelector('#rows');
const message = document.querySelector('#message');
const summary = document.querySelector('#summary');

const euro = (amount) => `${amount.toFixed(2)} EUR`;

function cell(content) {
  const td = document.createElement('td');
  td.append(content);
  return td;
}

// The table is always rebuilt from the model.
function render() {
  rows.replaceChildren(
    ...inventory.items.map((item) => {
      const remove = document.createElement('button');
      remove.type = 'button';
      remove.textContent = 'Remove';
      remove.dataset.name = item.name;
      const row = document.createElement('tr');
      row.append(cell(item.name), cell(euro(item.price)), cell(String(item.quantity)), cell(euro(item.value)), cell(remove));
      return row;
    })
  );
  summary.textContent = `${inventory.count} items, total value ${euro(inventory.totalValue)}`;
}

form.addEventListener('submit', (event) => {
  event.preventDefault();
  try {
    const item = new Item(nameInput.value, Number(priceInput.value), Number(quantityInput.value));
    inventory.add(item);
    message.textContent = `Added ${item.name}.`;
    form.reset();
  } catch (error) {
    message.textContent = error.message;
  }
  render();
});

rows.addEventListener('click', (event) => {
  const button = event.target.closest('button');
  if (!button) return;
  try {
    inventory.remove(button.dataset.name);
    message.textContent = `Removed ${button.dataset.name}.`;
  } catch (error) {
    message.textContent = error.message;
  }
  render();
});

render();
Auf dem eigenen Computer ausführen

Installieren Sie ECMAScript 2026 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.js

class Item {
  #name;
  #price;
  #quantity;

  constructor(name, price, quantity) {
    if (typeof name !== 'string' || name.trim() === '') throw new TypeError('name must not be empty');
    if (!(price >= 0)) throw new RangeError(`invalid price: ${price}`);
    if (!Number.isInteger(quantity) || quantity < 0) throw new RangeError(`invalid quantity: ${quantity}`);
    this.#name = name.trim();
    this.#price = price;
    this.#quantity = quantity;
  }

  get name() {
    return this.#name;
  }

  get price() {
    return this.#price;
  }

  get quantity() {
    return this.#quantity;
  }

  get value() {
    return this.#price * this.#quantity;
  }
}

class Inventory {
  #items = [];

  static fromJSON(text) {
    const inventory = new Inventory();
    // TODO: parse text and add one Item for each entry.
    return inventory;
  }

  add(item) {
    if (!(item instanceof Item)) throw new TypeError('only an Item can be added');
    if (this.#items.some((other) => other.name === item.name)) {
      throw new Error(`${item.name} is already in the inventory`);
    }
    this.#items.push(item);
  }

  remove(name) {
    const index = this.#items.findIndex((item) => item.name === name);
    if (index === -1) throw new Error(`${name} is not in the inventory`);
    this.#items.splice(index, 1);
  }

  get items() {
    return [...this.#items];
  }

  get count() {
    return this.#items.length;
  }

  get totalValue() {
    return this.#items.reduce((sum, item) => sum + item.value, 0);
  }
}

const STOCK = '[{"name": "Pencil", "price": 0.5, "quantity": 40}, {"name": "Notebook", "price": 2.25, "quantity": 12}, {"name": "Stapler", "price": 7.9, "quantity": 3}]';
const inventory = Inventory.fromJSON(STOCK);

const form = document.querySelector('#add-form');
const nameInput = document.querySelector('#item-name');
const priceInput = document.querySelector('#item-price');
const quantityInput = document.querySelector('#item-quantity');
const rows = document.querySelector('#rows');
const message = document.querySelector('#message');
const summary = document.querySelector('#summary');

const euro = (amount) => `${amount.toFixed(2)} EUR`;

function cell(content) {
  const td = document.createElement('td');
  td.append(content);
  return td;
}

// The table is always rebuilt from the model.
function render() {
  rows.replaceChildren(
    ...inventory.items.map((item) => {
      const remove = document.createElement('button');
      remove.type = 'button';
      remove.textContent = 'Remove';
      remove.dataset.name = item.name;
      const row = document.createElement('tr');
      row.append(cell(item.name), cell(euro(item.price)), cell(String(item.quantity)), cell(euro(item.value)), cell(remove));
      return row;
    })
  );
  summary.textContent = `${inventory.count} items, total value ${euro(inventory.totalValue)}`;
}

form.addEventListener('submit', (event) => {
  event.preventDefault();
  try {
    const item = new Item(nameInput.value, Number(priceInput.value), Number(quantityInput.value));
    inventory.add(item);
    message.textContent = `Added ${item.name}.`;
    form.reset();
  } catch (error) {
    message.textContent = error.message;
  }
  render();
});

rows.addEventListener('click', (event) => {
  const button = event.target.closest('button');
  if (!button) return;
  try {
    inventory.remove(button.dataset.name);
    message.textContent = `Removed ${button.dataset.name}.`;
  } catch (error) {
    message.textContent = error.message;
  }
  render();
});

render();

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {loadPage} from './learnrun.js';

const text = (page, selector) => page.document.querySelector(selector).textContent;
const names = (page) => [...page.document.querySelectorAll('#rows tr')].map((row) => row.firstChild.textContent);
function submit(page, name, price, quantity) {
  page.document.querySelector('#item-name').value = name;
  page.document.querySelector('#item-price').value = price;
  page.document.querySelector('#item-quantity').value = quantity;
  page.document.querySelector('#add-form button').click();
}
function removeButton(page, name) {
  const button = page.document.querySelector(`#rows button[data-name="${name}"]`);
  assert.ok(button, `es gibt keinen Remove-Button für ${name}; die Zeilen sind ${JSON.stringify(names(page))}`);
  return button;
}

test('Die Seite beginnt mit den drei Artikeln des Bestands', async () => {
  const page = await loadPage();
  assert.deepEqual(names(page), ['Pencil', 'Notebook', 'Stapler'], 'die Zeilen sind ' + JSON.stringify(names(page)));
});

test('Die Zusammenfassung zeigt Anzahl und Gesamtwert', async () => {
  const page = await loadPage();
  assert.equal(text(page, '#summary'), '3 items, total value 70.70 EUR', 'die Zusammenfassung lautet ' + text(page, '#summary'));
});

test('Die Spalte Value ist Preis mal Menge', async () => {
  const page = await loadPage();
  const row = removeButton(page, 'Stapler').closest('tr');
  const cells = [...row.children].map((td) => td.textContent);
  assert.deepEqual(cells.slice(0, 4), ['Stapler', '7.90 EUR', '3', '23.70 EUR'], 'die Zeile Stapler ist ' + JSON.stringify(cells));
});

index.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Inventory</title>
</head>
<body>
  <h1>Inventory</h1>
  <form id="add-form" novalidate>
    <label>Name <input id="item-name"></label>
    <label>Price <input id="item-price" type="number" step="0.01"></label>
    <label>Quantity <input id="item-quantity" type="number"></label>
    <button type="submit">Add</button>
  </form>
  <p id="message"></p>
  <table>
    <thead>
      <tr><th>Item</th><th>Price</th><th>Quantity</th><th>Value</th><th></th></tr>
    </thead>
    <tbody id="rows"></tbody>
  </table>
  <p id="summary"></p>
  <script type="module" src="main.js"></script>
</body>
</html>

package.json

{
  "type": "module"
}

package.json sagt Node.js, dass die .js-Dateien Module sind; lassen Sie die Datei im Ordner.

Programm ausführen:

Um die Seite auf Ihrem Computer auszuführen, öffnen Sie index.html über einen lokalen Webserver, nicht als Datei: Browser führen Modul-Skripte von file://-Adressen nicht aus. Für die Prüfungen installieren Sie zuerst jsdom (die Befehle unten).

Prüfungen ausführen (learnrun.js muss im selben Ordner liegen):

npm install jsdom@28.1.0
node --test
learnrun.js herunterladen

Häufige Fehler

Einfache Daten statt eines Items hinzufügen

class Item {
  #name;
  constructor(name) {
    this.#name = name;
  }
  get name() {
    return this.#name;
  }
}
class Inventory {
  #items = [];
  add(item) {
    if (!(item instanceof Item)) throw new TypeError("only an Item can be added");
    this.#items.push(item);
  }
}
const inventory = new Inventory();
inventory.add({name: "Pencil", price: 0.5, quantity: 40});

Was Node.js ausgibt

TypeError: only an Item can be added

Warum, und die Lösung

Das Objektliteral hat die richtigen Eigenschaften, wurde aber nie vom constructor von Item geprüft, deshalb lehnt das Inventar es mit einem eigenen TypeError ab. Bauen Sie zuerst das Item: inventory.add(new Item("Pencil", 0.5, 40)). Deshalb ruft fromJSON für jeden Eintrag new Item auf, statt die geparsten Objekte hinzuzufügen.

Eine Eigenschaft nur mit Getter von außen ändern

class Item {
  #quantity;
  constructor(quantity) {
    this.#quantity = quantity;
  }
  get quantity() {
    return this.#quantity;
  }
}
const pencil = new Item(40);
pencil.quantity = 39;

Was Node.js ausgibt

TypeError: Cannot set property quantity of #<Item> which has only a getter

Warum, und die Lösung

quantity hat einen Getter, aber keinen Setter, ist also schreibgeschützt, und in einem Modul wirft die Zuweisung. Das ist Absicht: Änderungen sollen über eine Methode laufen, die sie prüft. Fügen Sie eine hinzu, etwa take(count), die wirft, wenn count größer als #quantity ist, und sie sonst verringert.

Die Factory auf einer Instanz aufrufen

class Inventory {
  #items = [];
  static fromJSON(text) {
    const inventory = new Inventory();
    inventory.#items.push(...JSON.parse(text));
    return inventory;
  }
}
const empty = new Inventory();
const stock = empty.fromJSON('[{"name": "Pencil"}]');

Was Node.js ausgibt

TypeError: empty.fromJSON is not a function

Warum, und die Lösung

fromJSON ist statisch, gehört also zur Klasse Inventory und nicht zu ihren Instanzen. Sie brauchen kein Inventar, um ein Inventar zu erzeugen: Rufen Sie Inventory.fromJSON('[{"name": "Pencil"}]') auf.

JavaScript im Browser: die eigene Engine Ihres Browsers in einem abgeschotteten Worker. Syntaxfehler findet acorn 8.18.0, MIT. Lizenz und Quellcode

Abschlussquiz

5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.

Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.

Problem melden

Etwas ist falsch oder unklar? Beschreiben Sie es kurz, dann wird es geprüft und korrigiert.

#

Mindestens 20 Zeichen.

Nur, wenn Sie eine Antwort wünschen.

Kernideen

Zwei Klassen halten die Daten

Item hält name, price und quantity in privaten Feldern und bietet sie über Getter an, sodass nichts außerhalb einen Artikel nach seiner Erzeugung ändern kann. Sein Getter value berechnet bei jedem Lesen price × quantity. Inventory hält seine Artikel in einem privaten Array #items. Sein Getter items gibt eine Kopie zurück, [...this.#items], denn würde er das Array selbst zurückgeben, könnte jeder Code etwas hineinschieben. count und totalValue sind ebenfalls Getter. Die statische Factory Inventory.fromJSON(text) parst JSON und fügt je Eintrag ein Item hinzu.

Das Modell lehnt schlechte Daten ab

Die Prüfungen liegen in den Klassen, nicht in der Seite. Der constructor von Item wirft einen TypeError für einen leeren Namen und einen RangeError für einen Preis, der nicht >= 0 ist, oder eine Menge, die keine ganze Zahl ist. !(price >= 0) fängt auch NaN, das price < 0 durchließe. Das add von Inventory wirft für einen Namen, den es schon hat, remove für einen, den es nicht hat. Die Seite umgibt jede Änderung mit try...catch und zeigt error.message an, sodass ein abgelehnter Artikel nie in die Tabelle gelangt.

Eine einzige Quelle der Wahrheit

Die Seite ändert nie direkt eine Tabellenzeile. Jede Änderung geht zuerst an das Inventar; dann baut render() mit replaceChildren alle Zeilen und die Zusammenfassung aus inventory.items neu auf, die Tabelle zeigt also immer, was das Modell hält. Der submit-Listener des Formulars ruft preventDefault auf, baut aus den Eingaben ein Item und fügt es hinzu. Ein einziger click-Listener am Tabellenkörper bedient jeden Remove-Button: closest('button') findet den Button, und sein data-name sagt, welcher Artikel entfernt werden soll.

Quellen

Zuletzt geprüft am 5. Oktober 2026