Aufwärmen · Aufgabe 1 von 7
Zum Aufwärmen aus Lektion I1.5. Was gibt dieser Code aus?
const {promise, resolve} = Promise.withResolvers();
resolve("a");
resolve("b");
console.log(await promise);// I1.6 · ca. 40 Min. · Aufbau
Sie bauen eine Quiz-Seite, in der jede Frage auf einen Klick oder auf den Countdown wartet, je nachdem, was zuerst kommt: zwei Promises, ein Promise.race und eine Schleife mit await.
Lektion 6 von 6 in I1 Asynchrones JavaScript
Danach können Sie
Aufwärmen · Aufgabe 1 von 7
const {promise, resolve} = Promise.withResolvers();
resolve("a");
resolve("b");
console.log(await promise);Vorhersagen · Aufgabe 2 von 7
const noAnswer = new Promise(() => {}); // nobody clicks
const timeUp = new Promise((resolve) => setTimeout(() => resolve(null), 20));
const answer = await Promise.race([noAnswer, timeUp]);
console.log(answer === null ? "Time's up!" : answer);Üben · Aufgabe 3 von 7
function countdown(seconds, tick) {
let left = seconds;
const id = setInterval(() => (left -= 1), tick);
return {stop: () => ____(id), left: () => left};
}Üben · Aufgabe 4 von 7
Üben · Aufgabe 5 von 7
const ask = () => new Promise((resolve) => setTimeout(() => resolve("reject"), 10));
const timeUp = () => new Promise((resolve) => setTimeout(() => resolve(null), 30));
const answer = await Promise.race([ask(), timeUp()]);
console.log(answer === null ? "Time's up!" : answer === "reject" ? "Right!" : "Wrong.");Denksport · Aufgabe 6 von 7
const questions = ["q1", "q2"];
questions.forEach(async (question) => {
console.log("ask", question);
await null;
console.log("answered", question);
});
console.log("score");Anwenden · Aufgabe 7 von 7
Prüfen Sie Ihr Ergebnis anhand dieser Liste
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
Die Seite hat eine Frage, ein leeres #answers-Feld, einen Timer, eine Ergebniszeile und einen Start-Button. main.js enthält drei Fragen als Daten. ask() und countdown() geben Promises zurück, und run() lässt sie für jede Frage in einer for...of-Schleife gegeneinander antreten. Am Ende spielt das Skript sein eigenes Quiz, so wie es die Tests der Übungen tun: Es beantwortet die erste Frage richtig, die zweite falsch und lässt bei der dritten die Zeit ablaufen.
main.js
const questions = [
{ text: 'Which combinator waits for every promise to fulfil?', options: ['Promise.race', 'Promise.all', 'Promise.any'], answer: 'Promise.all' },
{ text: 'Which function makes a new promise fail?', options: ['resolve', 'reject', 'finally'], answer: 'reject' },
{ text: 'What does await do with a rejected promise?', options: ['It gives undefined', 'It throws', 'It waits forever'], answer: 'It throws' }
];
const SECONDS = 3; // per question
const questionText = document.querySelector('#question');
const answers = document.querySelector('#answers');
const timerText = document.querySelector('#timer');
const result = document.querySelector('#result');
const startButton = document.querySelector('#start');
// Shows a question and its answer buttons.
// The promise resolves with the text of the first button clicked.
function ask(question) {
questionText.textContent = question.text;
const buttons = question.options.map((option) => {
const button = document.createElement('button');
button.type = 'button';
button.textContent = option;
return button;
});
answers.replaceChildren(...buttons);
return new Promise((resolve) => {
for (const button of buttons) {
button.addEventListener('click', () => resolve(button.textContent), { once: true });
}
});
}
// Counts down once a second. The promise resolves with null when the time is up;
// stop() ends the countdown early.
function countdown(seconds) {
const { promise, resolve } = Promise.withResolvers();
let left = seconds;
timerText.textContent = `${left} s left`;
const id = setInterval(() => {
left -= 1;
timerText.textContent = `${left} s left`;
if (left === 0) {
clearInterval(id);
resolve(null);
}
}, 1000);
return { promise, stop: () => clearInterval(id) };
}
async function run() {
startButton.disabled = true;
result.textContent = '';
let score = 0;
for (const question of questions) {
const timer = countdown(SECONDS);
const answer = await Promise.race([ask(question), timer.promise]);
timer.stop();
if (answer === question.answer) score += 1;
result.textContent = answer === null ? "Time's up!" : answer === question.answer ? 'Right!' : 'Wrong.';
}
questionText.textContent = 'Done.';
answers.replaceChildren();
timerText.textContent = '';
result.textContent = `Score: ${score} of ${questions.length}`;
startButton.disabled = false;
}
startButton.addEventListener('click', run);
// The script plays its own quiz, the way the exercise tests do.
const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const click = (label) => [...answers.querySelectorAll('button')].find((b) => b.textContent === label).click();
startButton.click();
console.log('1:', questionText.textContent, '|', timerText.textContent);
click('Promise.all');
await pause(0);
console.log('->', result.textContent);
click('resolve');
await pause(0);
console.log('->', result.textContent);
console.log('3:', questionText.textContent);
await pause(3200); // no answer
console.log('->', questionText.textContent, '|', result.textContent);
index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Quick quiz</title>
</head>
<body>
<h1>Quick quiz</h1>
<p id="question">Press Start.</p>
<div id="answers"></div>
<p id="timer"></p>
<p id="result"></p>
<button id="start" type="button">Start</button>
<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
1: Which combinator waits for every promise to fulfil? | 3 s left
-> Right!
-> Wrong.
3: What does await do with a rejected promise?
-> Done. | Score: 1 of 3
<!-- #result -->
<p id="result">Score: 1 of 3</p>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.
Übung 1 von 3
Vervollständigen Sie ask(question). Es zeigt schon die Frage und ihre Antwort-Buttons. Lassen Sie es ein Promise zurückgeben, das mit dem Text des ersten angeklickten Buttons erledigt wird; geben Sie jedem Button einen Klick-Listener mit {once: true}, der resolve aufruft. Der Startcode gibt nichts zurück, das Quiz rast deshalb durch alle Fragen auf einmal.
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.
Führen Sie die Tests aus: Die erste Frage ist schon weg. Was bekommt run(), wenn es auf eine Funktion wartet, die nichts zurückgibt?
Geben Sie am Ende von ask new Promise((resolve) => { … }) zurück und fügen Sie die Listener darin hinzu.
for (const button of buttons) button.addEventListener('click', () => resolve(button.textContent), { once: true });
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
const questions = [
{ text: 'Which combinator waits for every promise to fulfil?', options: ['Promise.race', 'Promise.all', 'Promise.any'], answer: 'Promise.all' },
{ text: 'Which function makes a new promise fail?', options: ['resolve', 'reject', 'finally'], answer: 'reject' },
{ text: 'What does await do with a rejected promise?', options: ['It gives undefined', 'It throws', 'It waits forever'], answer: 'It throws' }
];
const SECONDS = 3; // per question
const questionText = document.querySelector('#question');
const answers = document.querySelector('#answers');
const timerText = document.querySelector('#timer');
const result = document.querySelector('#result');
const startButton = document.querySelector('#start');
// Shows a question and its answer buttons.
// The promise resolves with the text of the first button clicked.
function ask(question) {
questionText.textContent = question.text;
const buttons = question.options.map((option) => {
const button = document.createElement('button');
button.type = 'button';
button.textContent = option;
return button;
});
answers.replaceChildren(...buttons);
return new Promise((resolve) => {
for (const button of buttons) {
button.addEventListener('click', () => resolve(button.textContent), { once: true });
}
});
}
// Counts down once a second. The promise resolves with null when the time is up;
// stop() ends the countdown early.
function countdown(seconds) {
const { promise, resolve } = Promise.withResolvers();
let left = seconds;
timerText.textContent = `${left} s left`;
const id = setInterval(() => {
left -= 1;
timerText.textContent = `${left} s left`;
if (left === 0) {
clearInterval(id);
resolve(null);
}
}, 1000);
return { promise, stop: () => clearInterval(id) };
}
async function run() {
startButton.disabled = true;
result.textContent = '';
let score = 0;
for (const question of questions) {
const timer = countdown(SECONDS);
const answer = await Promise.race([ask(question), timer.promise]);
timer.stop();
if (answer === question.answer) score += 1;
result.textContent = answer === null ? "Time's up!" : answer === question.answer ? 'Right!' : 'Wrong.';
}
questionText.textContent = 'Done.';
answers.replaceChildren();
timerText.textContent = '';
result.textContent = `Score: ${score} of ${questions.length}`;
startButton.disabled = false;
}
startButton.addEventListener('click', run);
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
const questions = [
{ text: 'Which combinator waits for every promise to fulfil?', options: ['Promise.race', 'Promise.all', 'Promise.any'], answer: 'Promise.all' },
{ text: 'Which function makes a new promise fail?', options: ['resolve', 'reject', 'finally'], answer: 'reject' },
{ text: 'What does await do with a rejected promise?', options: ['It gives undefined', 'It throws', 'It waits forever'], answer: 'It throws' }
];
const SECONDS = 3; // per question
const questionText = document.querySelector('#question');
const answers = document.querySelector('#answers');
const timerText = document.querySelector('#timer');
const result = document.querySelector('#result');
const startButton = document.querySelector('#start');
// Shows a question and its answer buttons.
// The promise resolves with the text of the first button clicked.
function ask(question) {
questionText.textContent = question.text;
const buttons = question.options.map((option) => {
const button = document.createElement('button');
button.type = 'button';
button.textContent = option;
return button;
});
answers.replaceChildren(...buttons);
// TODO: return a promise that resolves with the text of the first button clicked.
}
// Counts down once a second. The promise resolves with null when the time is up;
// stop() ends the countdown early.
function countdown(seconds) {
const { promise, resolve } = Promise.withResolvers();
let left = seconds;
timerText.textContent = `${left} s left`;
const id = setInterval(() => {
left -= 1;
timerText.textContent = `${left} s left`;
if (left === 0) {
clearInterval(id);
resolve(null);
}
}, 1000);
return { promise, stop: () => clearInterval(id) };
}
async function run() {
startButton.disabled = true;
result.textContent = '';
let score = 0;
for (const question of questions) {
const timer = countdown(SECONDS);
const answer = await Promise.race([ask(question), timer.promise]);
timer.stop();
if (answer === question.answer) score += 1;
result.textContent = answer === null ? "Time's up!" : answer === question.answer ? 'Right!' : 'Wrong.';
}
questionText.textContent = 'Done.';
answers.replaceChildren();
timerText.textContent = '';
result.textContent = `Score: ${score} of ${questions.length}`;
startButton.disabled = false;
}
startButton.addEventListener('click', run);
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {loadPage} from './learnrun.js';
const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const text = (page, selector) => page.document.querySelector(selector).textContent;
const labels = (page) => [...page.document.querySelectorAll('#answers button')].map((b) => b.textContent);
async function answer(page, label) {
const button = [...page.document.querySelectorAll('#answers button')].find((b) => b.textContent === label);
assert.ok(button, `es gibt keinen Antwort-Button ${label}; die Buttons sind ${JSON.stringify(labels(page))}`);
button.click();
await pause(0);
}
async function start() {
const page = await loadPage();
page.document.querySelector('#start').click();
await pause(0);
return page;
}
test('Start zeigt die erste Frage und ihre drei Antworten', async () => {
const page = await start();
assert.equal(text(page, '#question'), 'Which combinator waits for every promise to fulfil?', 'die Frage lautet ' + text(page, '#question'));
assert.deepEqual(labels(page), ['Promise.race', 'Promise.all', 'Promise.any'], 'die Antwort-Buttons sind ' + JSON.stringify(labels(page)));
});
test('Eine richtige Antwort sagt Right! und zeigt die nächste Frage', async () => {
const page = await start();
await answer(page, 'Promise.all');
assert.equal(text(page, '#result'), 'Right!', 'das Ergebnis ist ' + text(page, '#result'));
assert.equal(text(page, '#question'), 'Which function makes a new promise fail?', 'die Frage lautet ' + text(page, '#question'));
});
test('Eine falsche Antwort sagt Wrong.', async () => {
const page = await start();
await answer(page, 'Promise.race');
assert.equal(text(page, '#result'), 'Wrong.', 'das Ergebnis ist ' + text(page, '#result'));
});
index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Quick quiz</title>
</head>
<body>
<h1>Quick quiz</h1>
<p id="question">Press Start.</p>
<div id="answers"></div>
<p id="timer"></p>
<p id="result"></p>
<button id="start" type="button">Start</button>
<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 --testlearnrun.js herunterladenÜbung 2 von 3
Vervollständigen Sie countdown(seconds). Es zeigt schon die verbleibenden Sekunden und zieht jede Sekunde eine ab. Erreicht left die 0, muss es das Intervall mit clearInterval stoppen und das Promise mit null erledigen, damit das race mit "Time’s up!" endet. Der Startcode wird nie erledigt, eine Frage ohne Antwort wartet also ewig, während der Timer unter null zählt.
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.
Führen Sie die Tests aus: Nach 3 Sekunden zeigt der Timer 0 s left und geht dann unter null. Was soll bei 0 geschehen?
Im Callback des Intervalls, nach dem Aktualisieren des Textes: if (left === 0) { … }.
if (left === 0) { clearInterval(id); resolve(null); }
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
const questions = [
{ text: 'Which combinator waits for every promise to fulfil?', options: ['Promise.race', 'Promise.all', 'Promise.any'], answer: 'Promise.all' },
{ text: 'Which function makes a new promise fail?', options: ['resolve', 'reject', 'finally'], answer: 'reject' },
{ text: 'What does await do with a rejected promise?', options: ['It gives undefined', 'It throws', 'It waits forever'], answer: 'It throws' }
];
const SECONDS = 3; // per question
const questionText = document.querySelector('#question');
const answers = document.querySelector('#answers');
const timerText = document.querySelector('#timer');
const result = document.querySelector('#result');
const startButton = document.querySelector('#start');
// Shows a question and its answer buttons.
// The promise resolves with the text of the first button clicked.
function ask(question) {
questionText.textContent = question.text;
const buttons = question.options.map((option) => {
const button = document.createElement('button');
button.type = 'button';
button.textContent = option;
return button;
});
answers.replaceChildren(...buttons);
return new Promise((resolve) => {
for (const button of buttons) {
button.addEventListener('click', () => resolve(button.textContent), { once: true });
}
});
}
// Counts down once a second. The promise resolves with null when the time is up;
// stop() ends the countdown early.
function countdown(seconds) {
const { promise, resolve } = Promise.withResolvers();
let left = seconds;
timerText.textContent = `${left} s left`;
const id = setInterval(() => {
left -= 1;
timerText.textContent = `${left} s left`;
if (left === 0) {
clearInterval(id);
resolve(null);
}
}, 1000);
return { promise, stop: () => clearInterval(id) };
}
async function run() {
startButton.disabled = true;
result.textContent = '';
let score = 0;
for (const question of questions) {
const timer = countdown(SECONDS);
const answer = await Promise.race([ask(question), timer.promise]);
timer.stop();
if (answer === question.answer) score += 1;
result.textContent = answer === null ? "Time's up!" : answer === question.answer ? 'Right!' : 'Wrong.';
}
questionText.textContent = 'Done.';
answers.replaceChildren();
timerText.textContent = '';
result.textContent = `Score: ${score} of ${questions.length}`;
startButton.disabled = false;
}
startButton.addEventListener('click', run);
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
const questions = [
{ text: 'Which combinator waits for every promise to fulfil?', options: ['Promise.race', 'Promise.all', 'Promise.any'], answer: 'Promise.all' },
{ text: 'Which function makes a new promise fail?', options: ['resolve', 'reject', 'finally'], answer: 'reject' },
{ text: 'What does await do with a rejected promise?', options: ['It gives undefined', 'It throws', 'It waits forever'], answer: 'It throws' }
];
const SECONDS = 3; // per question
const questionText = document.querySelector('#question');
const answers = document.querySelector('#answers');
const timerText = document.querySelector('#timer');
const result = document.querySelector('#result');
const startButton = document.querySelector('#start');
// Shows a question and its answer buttons.
// The promise resolves with the text of the first button clicked.
function ask(question) {
questionText.textContent = question.text;
const buttons = question.options.map((option) => {
const button = document.createElement('button');
button.type = 'button';
button.textContent = option;
return button;
});
answers.replaceChildren(...buttons);
return new Promise((resolve) => {
for (const button of buttons) {
button.addEventListener('click', () => resolve(button.textContent), { once: true });
}
});
}
// Counts down once a second. The promise resolves with null when the time is up;
// stop() ends the countdown early.
function countdown(seconds) {
const { promise, resolve } = Promise.withResolvers();
let left = seconds;
timerText.textContent = `${left} s left`;
const id = setInterval(() => {
left -= 1;
timerText.textContent = `${left} s left`;
// TODO: at 0, stop the interval and resolve the promise with null.
}, 1000);
return { promise, stop: () => clearInterval(id) };
}
async function run() {
startButton.disabled = true;
result.textContent = '';
let score = 0;
for (const question of questions) {
const timer = countdown(SECONDS);
const answer = await Promise.race([ask(question), timer.promise]);
timer.stop();
if (answer === question.answer) score += 1;
result.textContent = answer === null ? "Time's up!" : answer === question.answer ? 'Right!' : 'Wrong.';
}
questionText.textContent = 'Done.';
answers.replaceChildren();
timerText.textContent = '';
result.textContent = `Score: ${score} of ${questions.length}`;
startButton.disabled = false;
}
startButton.addEventListener('click', run);
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {loadPage} from './learnrun.js';
const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const text = (page, selector) => page.document.querySelector(selector).textContent;
const labels = (page) => [...page.document.querySelectorAll('#answers button')].map((b) => b.textContent);
async function answer(page, label) {
const button = [...page.document.querySelectorAll('#answers button')].find((b) => b.textContent === label);
assert.ok(button, `es gibt keinen Antwort-Button ${label}; die Buttons sind ${JSON.stringify(labels(page))}`);
button.click();
await pause(0);
}
async function start() {
const page = await loadPage();
page.document.querySelector('#start').click();
await pause(0);
return page;
}
test('Der Timer beginnt bei 3 s left und zeigt eine Sekunde später 2 s left', async () => {
const page = await start();
assert.equal(text(page, '#timer'), '3 s left', 'am Anfang zeigt der Timer ' + text(page, '#timer'));
await pause(1100);
assert.equal(text(page, '#timer'), '2 s left', 'nach einer Sekunde zeigt der Timer ' + text(page, '#timer'));
});
test('Nach 3 Sekunden ohne Antwort ist die Zeit um, und die nächste Frage erscheint', async () => {
const page = await start();
try {
await pause(3300);
assert.equal(text(page, '#result'), "Time's up!", 'das Ergebnis ist ' + text(page, '#result'));
assert.equal(text(page, '#question'), 'Which function makes a new promise fail?', 'die Frage lautet ' + text(page, '#question'));
assert.equal(text(page, '#timer'), '3 s left', 'der nächste Countdown zeigt ' + text(page, '#timer'));
} finally {
page.window.close(); // stops a timer that never ends, so the test run can finish
}
});
index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Quick quiz</title>
</head>
<body>
<h1>Quick quiz</h1>
<p id="question">Press Start.</p>
<div id="answers"></div>
<p id="timer"></p>
<p id="result"></p>
<button id="start" type="button">Start</button>
<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 --testlearnrun.js herunterladenÜbung 3 von 3
Reparieren Sie run(). Der Startcode benutzt questions.forEach mit einem async-Callback, und forEach wartet nicht darauf: Alle drei Fragen werden auf einmal gestellt, und die Punktzahl erscheint sofort. Benutzen Sie eine for...of-Schleife, damit jedes await run() anhält, bis diese Frage entschieden ist. Start muss gesperrt bleiben, bis die Punktzahl angezeigt wird.
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.
Führen Sie die Tests aus: Die Seite zeigt sofort die dritte Frage und die Punktzahl. Wer wartet auf die Promises, die der forEach-Callback zurückgibt?
Ersetzen Sie questions.forEach(async (question) => { … }); durch for (const question of questions) { … } und behalten Sie den Rumpf.
Das await in for...of hält run() selbst an, die Zeilen nach der Schleife laufen also erst nach der letzten Frage.
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
const questions = [
{ text: 'Which combinator waits for every promise to fulfil?', options: ['Promise.race', 'Promise.all', 'Promise.any'], answer: 'Promise.all' },
{ text: 'Which function makes a new promise fail?', options: ['resolve', 'reject', 'finally'], answer: 'reject' },
{ text: 'What does await do with a rejected promise?', options: ['It gives undefined', 'It throws', 'It waits forever'], answer: 'It throws' }
];
const SECONDS = 3; // per question
const questionText = document.querySelector('#question');
const answers = document.querySelector('#answers');
const timerText = document.querySelector('#timer');
const result = document.querySelector('#result');
const startButton = document.querySelector('#start');
// Shows a question and its answer buttons.
// The promise resolves with the text of the first button clicked.
function ask(question) {
questionText.textContent = question.text;
const buttons = question.options.map((option) => {
const button = document.createElement('button');
button.type = 'button';
button.textContent = option;
return button;
});
answers.replaceChildren(...buttons);
return new Promise((resolve) => {
for (const button of buttons) {
button.addEventListener('click', () => resolve(button.textContent), { once: true });
}
});
}
// Counts down once a second. The promise resolves with null when the time is up;
// stop() ends the countdown early.
function countdown(seconds) {
const { promise, resolve } = Promise.withResolvers();
let left = seconds;
timerText.textContent = `${left} s left`;
const id = setInterval(() => {
left -= 1;
timerText.textContent = `${left} s left`;
if (left === 0) {
clearInterval(id);
resolve(null);
}
}, 1000);
return { promise, stop: () => clearInterval(id) };
}
async function run() {
startButton.disabled = true;
result.textContent = '';
let score = 0;
for (const question of questions) {
const timer = countdown(SECONDS);
const answer = await Promise.race([ask(question), timer.promise]);
timer.stop();
if (answer === question.answer) score += 1;
result.textContent = answer === null ? "Time's up!" : answer === question.answer ? 'Right!' : 'Wrong.';
}
questionText.textContent = 'Done.';
answers.replaceChildren();
timerText.textContent = '';
result.textContent = `Score: ${score} of ${questions.length}`;
startButton.disabled = false;
}
startButton.addEventListener('click', run);
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
const questions = [
{ text: 'Which combinator waits for every promise to fulfil?', options: ['Promise.race', 'Promise.all', 'Promise.any'], answer: 'Promise.all' },
{ text: 'Which function makes a new promise fail?', options: ['resolve', 'reject', 'finally'], answer: 'reject' },
{ text: 'What does await do with a rejected promise?', options: ['It gives undefined', 'It throws', 'It waits forever'], answer: 'It throws' }
];
const SECONDS = 3; // per question
const questionText = document.querySelector('#question');
const answers = document.querySelector('#answers');
const timerText = document.querySelector('#timer');
const result = document.querySelector('#result');
const startButton = document.querySelector('#start');
// Shows a question and its answer buttons.
// The promise resolves with the text of the first button clicked.
function ask(question) {
questionText.textContent = question.text;
const buttons = question.options.map((option) => {
const button = document.createElement('button');
button.type = 'button';
button.textContent = option;
return button;
});
answers.replaceChildren(...buttons);
return new Promise((resolve) => {
for (const button of buttons) {
button.addEventListener('click', () => resolve(button.textContent), { once: true });
}
});
}
// Counts down once a second. The promise resolves with null when the time is up;
// stop() ends the countdown early.
function countdown(seconds) {
const { promise, resolve } = Promise.withResolvers();
let left = seconds;
timerText.textContent = `${left} s left`;
const id = setInterval(() => {
left -= 1;
timerText.textContent = `${left} s left`;
if (left === 0) {
clearInterval(id);
resolve(null);
}
}, 1000);
return { promise, stop: () => clearInterval(id) };
}
async function run() {
startButton.disabled = true;
result.textContent = '';
let score = 0;
// TODO: ask one question at a time; forEach does not wait for the await inside.
questions.forEach(async (question) => {
const timer = countdown(SECONDS);
const answer = await Promise.race([ask(question), timer.promise]);
timer.stop();
if (answer === question.answer) score += 1;
result.textContent = answer === null ? "Time's up!" : answer === question.answer ? 'Right!' : 'Wrong.';
});
questionText.textContent = 'Done.';
answers.replaceChildren();
timerText.textContent = '';
result.textContent = `Score: ${score} of ${questions.length}`;
startButton.disabled = false;
}
startButton.addEventListener('click', run);
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {loadPage} from './learnrun.js';
const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const text = (page, selector) => page.document.querySelector(selector).textContent;
const labels = (page) => [...page.document.querySelectorAll('#answers button')].map((b) => b.textContent);
async function answer(page, label) {
const button = [...page.document.querySelectorAll('#answers button')].find((b) => b.textContent === label);
assert.ok(button, `es gibt keinen Antwort-Button ${label}; die Buttons sind ${JSON.stringify(labels(page))}`);
button.click();
await pause(0);
}
async function start() {
const page = await loadPage();
page.document.querySelector('#start').click();
await pause(0);
return page;
}
test('Start zeigt nur die erste Frage, und Start ist gesperrt', async () => {
const page = await start();
assert.equal(text(page, '#question'), 'Which combinator waits for every promise to fulfil?', 'die Frage lautet ' + text(page, '#question'));
assert.equal(page.document.querySelector('#start').disabled, true, 'Start sollte gesperrt sein, solange das Quiz läuft');
});
test('Drei Antworten ergeben die Punktzahl, und Start funktioniert wieder', async () => {
const page = await start();
await answer(page, 'Promise.all');
await answer(page, 'finally');
await answer(page, 'It throws');
assert.equal(text(page, '#question'), 'Done.', 'am Ende lautet die Fragezeile ' + text(page, '#question'));
assert.equal(text(page, '#result'), 'Score: 2 of 3', 'das Ergebnis ist ' + text(page, '#result'));
assert.equal(page.document.querySelector('#start').disabled, false, 'Start sollte nach dem Quiz freigegeben sein');
});
index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Quick quiz</title>
</head>
<body>
<h1>Quick quiz</h1>
<p id="question">Press Start.</p>
<div id="answers"></div>
<p id="timer"></p>
<p id="result"></p>
<button id="start" type="button">Start</button>
<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 --testlearnrun.js herunterladenfunction countdown(ms) {
const {promise, resolve} = Promise.withResolvers();
const id = setTimeout(() => resolve(null), ms);
return {promise, stop: () => clearTimeout(id)};
}
const noAnswer = new Promise(() => {});
const timer = countdown(20);
const answer = await Promise.race([noAnswer, timer]);
timer.stop();
console.log(answer.toLowerCase());
Was Node.js ausgibt
TypeError: answer.toLowerCase is not a functionWarum, und die Lösung
timer ist das Objekt {promise, stop}, kein Promise. race behandelt einen Wert, der kein Promise ist, als schon erfüllt, er gewinnt also sofort, und answer ist dieses Objekt, bevor jemand klicken könnte. Lassen Sie das Promise darin antreten: Promise.race([noAnswer, timer.promise]).
const noAnswer = new Promise(() => {});
const timeUp = new Promise((resolve) => setTimeout(() => resolve(null), 20));
const answer = await Promise.race([noAnswer, timeUp]);
console.log(answer.toLowerCase() === "reject" ? "Right!" : "Wrong.");
Was Node.js ausgibt
TypeError: Cannot read properties of null (reading 'toLowerCase')Warum, und die Lösung
Antwortet niemand rechtzeitig, gewinnt der Countdown, und answer ist null, und null hat keine Methoden. Prüfen Sie zuerst auf das Timeout: if (answer === null) zeigt "Time’s up!", und vergleichen Sie Antworten erst danach.
const [promise, resolve] = Promise.withResolvers();
setTimeout(() => resolve(null), 20);
console.log(await promise);
Was Node.js ausgibt
TypeError: object is not iterable (cannot read property Symbol(Symbol.iterator))Warum, und die Lösung
withResolvers gibt ein Objekt mit den Schlüsseln promise, resolve und reject zurück, und ein Objekt lässt sich nicht mit eckigen Klammern destrukturieren. Nehmen Sie geschweifte Klammern und die genauen Namen: const {promise, resolve} = Promise.withResolvers();
JavaScript im Browser: die eigene Engine Ihres Browsers in einem abgeschotteten Worker. Syntaxfehler findet acorn 8.18.0, MIT. Lizenz und Quellcode
5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.
Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.