JavaScript/Tutorials/Taschenrechner

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche

Das Beispiel baut mit Hilfe eines HTML-Formulars einen halbwegs "echt" aussehenden Taschenrechner auf. Der Anwender kann diesen Taschenrechner wie üblich bedienen. Außerdem berechnet dieser Taschenrechner auch Serienrechnungen wie 1+2+3+4+5+6+7+8+9 und beherrscht die Punkt-vor-Strich-Regel bei Rechnungen wie 1+2*3. Zusätzlich können die Wurzelfunktion, die Quadratfunktion, sowie der natürliche Logarithmus genutzt werden.

Vorüberlegungen

Dieser Taschenrechner wurde bereits 2001 von Stefan Münz vorgestellt. Damals wurden die Klickbuttons mit onclick-Attributen versehen, um die Funktionalität zu ermöglichen. Um eine zeitgemäßere dynamische Ereignisbehandlung einzusetzen, wurde auf diese JavaScript-spezifischen Attribute verzichtet, um das Markup streng von der JavaScript-Programmlogik zu trennen. Außerdem wurde eine Tastaturunterstützung implementiert, mit der die Zahleneingabe und die vier Grundrechenarten wie gewohnt bedienbar sind.

HTML

Anhand dieses Beispiels können Sie studieren, wie man Formulareingaben mit JavaScript zur direkten Interaktion verwenden kann.

HTML-Gerüst ohne Funktion
<form id="calc">
	<output>0</output>
	<fieldset>
		<button></button>
		<button></button>
		<button>ln</button>
		<button>C</button>

		<button>7</button>
		<button>8</button>
		<button>9</button>
		<button>+</button>

		<button>4</button>
		<button>5</button>
		<button>6</button>
		<button>-</button>

		<button>1</button>
		<button>2</button>
		<button>3</button>
		<button>×</button>

		<button>0</button>
		<button>.</button>
		<button>=</button>
		<button>÷</button>
	</fieldset>
</form>

Unser Taschenrechner besteht aus einem Formular mit einem output-Element für das Display und einem fieldset mit 20 Buttons. Sie benötigen keine weiteren Attribute oder Klassen.


JavaScript

Schauen wir uns an, wie JavaScript mit dem obigen Dokument interagieren kann.

Initialisierung

Beispiel ansehen …
"use strict";

document.addEventListener("DOMContentLoaded", function () {

  var form = document.getElementById("calc"),
      out = document.querySelector("#calc output"),
      overwrite = true;

  //weiterer Code
  //...
});

Im Head findet sich ein Scriptbereich mit einer Handlerfunktion von DOMContentLoaded, in der die HTML-Elemente mit getElementById, bzw. querySelector identifiziert und Variablen zugewiesen werden. Sollten die benötigten Elemente nicht gefunden werden, so verweigert das Script jegliche weitere Ausführung.


Unterstützung für Internet Explorer

Im Internet Explorer sind manche der längst etablierten Möglichkeiten nicht nutzbar. So kann man die Array-Methode includes nicht benutzen, da im Internet Explorer Array-Objekte diese Methode nicht besitzen. Auch haben im Internet Explorer sogenannte live NodeLists keine Methode forEach, um über die Elemente der Liste mittels einer Callback-Funktion zu iterieren.

Abhilfe schafft hier ein sogenannter Polyfill:

Beispiel ansehen …
if (!Array.prototype.includes) {
	 
	Array.prototype.includes = function includes(searchElement) {
		if (this == null) {
			throw new TypeError('this is null or undefined');
		}

		var object = Object(this),
			length = object.length >>> 0;

		if (length === 0) {
			return false;
		}

		var start = arguments[1] >> 0,
			key = start < 0 ? Math.max(start + length, 0) : start,
			currentElement;

		while (key < length) {
			currentElement = object[key];

			if (searchElement === currentElement
				|| (
					searchElement !== searchElement
					&& currentElement !== currentElement
				)
			) {
				return true
			}

			key ++;
		}

		return false;
	};
	 
}

if (!NodeList.prototype.forEach) NodeList.prototype.forEach = Array.prototype.forEach;

Die erste if-Verzweigung testet, ob ein Array die Methode includes kennt. Ein solches Vorgehen nennt man feature detection. Ist sie nicht vorhanden, wird dem Prototypen von Array-Objekten eine solche Methode "beigebracht". Ähnlich verhält es sich bei der zweiten if-Verzweigung mit NodeList-Objekten, denen die Funktionalität dadurch gegeben wird, indem man die in Array-Objekten vorhandene forEach-Methode verwendet.


Absenden des Formulares verhindern

Da der Taschenrechner ein einzeiliges Formularfeld enthält, kann dieses Formular jederzeit mit Betätigung der Eingabetaste abgesendet werden. Ein Absenden ist im Zusammenhang mit dem Taschenrechner jedoch unerwünscht. Deshalb wird mit addEventListener eine Funktion an das submit-Event des <form>-Elements gebunden, die verhindert, dass der Browser das Standardverhalten beim Absenden eines Formulars ausführt:

Beispiel ansehen …
form.addEventListener("submit", function (ev) {
	// prevent form submission and page reload
	ev.preventDefault();
	ev.stopPropagation();
	return false;
});

Aus historischen Gründen kann es schon genügen, wenn die Handlerfunktion den Wert false zurück gibt. Um aber sicher zu verhindern, dass andere Elemente auf das Submit-Event reagieren, wird mit stopPropagation verhindert, dass die Bekanntgabe an diese stattfindet. Das Abschicken des Formulars, also das Standardverhalten im Ereignisfall von submit wird mit der Funktion preventDefault verhindert.


Klickfunktionalität

Innerhalb der DOMContentLoaded-Handlerfunktion befindet sich ab Zeile 175 ein Anweisungsblock, der den Buttons dynamisch eine Funktionalität zuweist.

Beispiel ansehen …
// button functionalities
document.querySelectorAll("#calc button").forEach(function (b) { 
	var c = b.textContent;

	switch (c) {

		case "9":
		case "8":
		case "7":
		case "6":
		case "5":
		case "4":
		case "3":
		case "2":
		case "1":
		case "0":
		case ".":
			b.addEventListener("click", function () {
				input(c);
			});
		break;

		case "+":
		case "-":
		case "×":
		case "÷":
			b.addEventListener("click", function () {
				operator(c);
			});
		break;

		case "√":
		case "x²":
		case "ln":
			b.addEventListener("click", function () {
				extra(c);
			});
		break;

		case "=":
			b.addEventListener("click", result);
		break;

		case "C":
			b.addEventListener("click", clear);
		break;
	}

Mit b.textContent wird der Textknoten der geklickten Buttons ausgelesen und in einer switch-Fallunterscheidung verarbeitet.

Falls es sich um Zahlen handelt, wird die Eingabe c, also der Textinhalt des jeweiligen Buttons, der Funktion input zur weiteren Verarbeitung übergeben.

Auch die arithmetischen Operatoren +-/* werden zur weiteren Verarbeitung einer Funktion (hier operator) übergeben.

Die mathematischen Sonderfunktionen , und ln werden über die Funktion extra aufgerufen.

Ein Klick auf = ruft die Funktion result auf.


Tastatureingabe

Um Tastatureingaben zu erhalten, lauscht unser Script auf das Ereignis keypress, indem wir mit addEventListener eine passende Funktion einrichten, die mögliche Tastatureingaben auswertet.

Tastatureingaben auswerten
document.addEventListener("keypress", function (ev) {

	// decimal point
	if ([44, 46].includes(ev.charCode)) {
		// , .
		input(".");
	}

	// digits
	if ([48, 49, 50, 51, 52, 53, 54, 55, 56, 57].includes(ev.charCode)) {
		// 0-9
		input(ev.charCode - 48);
	}

	// operators
	if ([42, 43, 45, 47].includes(ev.charCode)) {
		// * + - /
		operator(
			["×", "+", "-", "÷"][
				[42, 43, 45, 47].indexOf(ev.charCode)
			]
		);
	}

	// result
	if (ev.charCode == 61) {
		// =
		result();
	}

	// clear
	if ([67, 99].includes(ev.charCode)) {
		// C, c
		clear();
	}

	// logarithm
	if ([76, 108].includes(ev.charCode)) {
		// L, l
		extra("ln");
	}

	// root
	if ([82, 114].includes(ev.charCode)) {
		// R, r
		extra("√");
	}

	// square
	if ([83, 115].includes(ev.charCode)) {
		// S, s
		extra("x²");
	}

	// additional clear and result keys
	switch (ev.code) {
		// <delete> and <backspace> to clear display
		case "Backspace":
		case "Delete":
			clear();
		break;

		// both <enter> keys to display result
		case "Enter":
		case "NumpadEnter":
			result();
		break;
	}
});

Das Ereignis-Objekt wird in der Variablen ev in unserer Funktion ausgewertet, denn es enthält Eigenschaften, die Aufschluss darüber geben, welche Tasten benutzt wurden.

Bei den Zifferntasten ist es sinnvoll, ihren ASCII-Wert in der Eigenschaft charCode des Ereignis-Objekts zu verarbeiten. Gleiches gilt für die Tasten, die eine erweiterte Berechnung (Logarithmus, Wurzel oder Quadrat) auslösen sollen, oder die einen Dezimalpunkt bedeuten können (hier unterstützen wir , und .), sowie für das Istgleichzeichen.

Um eine Eingabetaste (Enter-Taste) zu ermitteln, nützt uns die Eigenschaft charCode nichts, da sie kein darstellbares Zeichen erzeugt. Hier braucht es die Eigenschaft code, in der sprichwörtlich der interne Name der Taste gespeichert ist. Um nun sowohl die Enter-Taste bei den Buchstaben, als auch die Enter-Taste auf dem Ziffernblock zu unterstützen, müssen beide Namen (hier Enter und NumpadEnter) verarbeitet werden. Ähnliches gilt auch für die Lösch-Tasten, mit denen unsere Anzeige wieder auf Null zurück gesetzt werden können soll.

Wenn man testen will, ob ein Array einen bestimmten Wert enthält (z.B. ob eine Zifferntaste gedrückt wurde), benutzt man die Methode includes, will man aber wissen, an welcher Stelle dieser Wert im Array steht (z.B. welche der Operatoren-Tasten benutzt wurde), verwendet man indexOf.


mathematische Sonderfunktionen

In der Funktion extra() werden „kompliziertere“ Berechnungen angestellt, die, wie Sie sehen werden, aber durch Methoden des Math-Objekts in jeweils nur einer Zeile erledigt sind.

Beispiel ansehen …
function extra (type) {

	switch (type) {

		case "√":
			out.textContent = Math.sqrt(result(true));
		break;

		case "x²":
			out.textContent = Math.pow(result(true), 2);
		break;

		case "ln":
			out.textContent = Math.log(result(true));
		break;
	}

	overwrite = true;
}

Für Wurzelberechnungen stellt das Math-Objekt die Funktion Math.sqrt() zu Verfügung.

Das Quadrat einer Zahl erreichen Sie mit einer Multiplikation mit sich selbst oder der Funktion Math.pow(result(true), 2).

Den Logarithmus ln berechnen Sie mit Math.log(result(true)).


Ergebnisberechnung

Bei einem Klick auf = (oder bei passender Benutzung der Tastatur) berechnet die Funktion result(noDisplay) das Ergebnis.

Beispiel ansehen …
function result (noDisplay) {
	var input = out.textContent,
		r = 0;

	// replace × with * and ÷ with / for eval()
	input = input.replace(/×/g, "*").replace(/÷/g, "/");

	// remove anything else that is not allowed here
	input = input.replace(/[^0-9. +\-*\/]/g, "");

	try {

		r = eval(input);

	} catch (e) {

		r = 0;
	}

	if (noDisplay !== true) {
		out.textContent = r;
		overwrite = true;
	}

	return r;
}

Mit einem RegEx werden nur Ziffern, der Dezimalpunkt und die vier Operatoren als gültige Zeichen für die Berechnung zugelassen (inklusive möglicher Leerzeichen). Dabei werden die üblichen Tastenbeschriftungen für Multiplikation und Division durch ihre JavaScript-Entsprechungen ersetzt.

Das eigentliche Rechenergebnis wird mit der window-Methode eval berechnet.