Fehlerlösung: Nicht erfasster TypeError
Nicht erfasster TypeError: document.getElementById (...) ist null
Ich habe eine einzelne JavaScript-Datei, die mit mehreren Seiten verbunden ist. Das folgende Snippet ist eine Funktion, die für eine einzelne Seite verwendet wird. Der obige Fehler wird angezeigt, wenn der Benutzer zu einer der Seiten navigiert, die die folgende Funktion nicht verwenden, und document.getElementById('button01').addEventListener('click', newThing);auf die untere Zeile verweist . Ich habe festgestellt, dass der Fehler aufgrund der Tatsache button01auftritt , dass er auf diesen Seiten nicht vorhanden ist.
function newThing() {
const output = document.getElementsByTagName('output')[0];
if (!(document.forms.thingSelection2.type.value in options)) {
return false;
}
const list = options[document.forms.thingSelection2.type.value];
const method = document.forms.thingSelection1.mode.value + 'Item';
const item = list[method]();
output.innerHTML = item;
}
document.getElementById('button01').addEventListener('click', newThing);
Lösung
Meine Lösung dafür ist einfach; Setzen Sie die Zeile in eine if-Anweisung wie folgt:
if(document.getElementById('button01')) {
document.getElementById('button01').addEventListener('click', newThing);
}
Dadurch wird der Fehler von den Seiten entfernt, die ihn nicht verwenden.
Fragen
Eröffnet dies die Möglichkeit für fehlerhaftes Verhalten, von dem ich nichts weiß? Gibt es einen besseren Weg, um die gleichen Ergebnisse zu erzielen?
HTML
Ich bin dankbar für das Feedback, das dieser Beitrag bisher erhalten hat. Da einige der Antworten Empfehlungen enthalten, die auf Annahmen meines HTML-Codes basieren, habe ich beschlossen, den entsprechenden HTML-Code im folgenden Snippet hinzuzufügen. Eine Sache zu beachten ist, dass ich zwei Formen verwende. Dies ist für die Funktionalität des Codes erforderlich. Mir sind keine unbeabsichtigten Fehler bekannt, die dies verursachen könnte. Wenn ja, lassen Sie es mich bitte unten wissen.
<div><output></output></div>
<div><button id="button01">New Thing</button></div>
<div>
<form name="thingSelection1">
<input type="radio" name="mode" value="random" id="mode1"> Random
<br/><input type="radio" name="mode" value="forward" id="mode2"> Old - New
<br/><input type="radio" name="mode" value="reverse" id="mode3"> New - Old
</form>
</div>
<div>
<form name="thingSelection2">
Doodle <input type="radio" name="type" value="doodle" id="doodleCheck"><br/>
Video <input type="radio" name="type" value="video" id="videoCheck"><br/>
Audio <input type="radio" name="type" value="audio" id="audioCheck"><br/>
Photo <input type="radio" name="type" value="photo" id="photoCheck"><br/>
Text <input type="radio" name="type" value="text" id="textCheck">
</form>
</div>
Darüber hinaus möchte ich darauf hinweisen, dass das ursprüngliche Snippet nur ein Teil meines vollständigen JavaScript ist. Aufgrund der Informationen in diesem Beitrag bin ich zu dem Schluss gekommen, dass das Hinzufügen des verbleibenden JavaScript, obwohl es möglicherweise für einige der folgenden Antworten nützlich und relevant ist, möglicherweise nicht in den Geltungsbereich des ursprünglichen Themas dieses Beitrags fällt. Bitte beachten Sie Ihre Gedanken dazu in den Kommentaren unten.
Antworten
Eröffnet dies die Möglichkeit für fehlerhaftes Verhalten, von dem ich nichts weiß?
Es ist nicht wahrscheinlich. Die ifAussage ist in Ordnung, kann aber sauberer gemacht werden:
Anstatt das Element zweimal auszuwählen (einmal, um zu überprüfen, ob es vorhanden ist, ein anderes Mal
addEventListener, um es aufzurufen ), speichern Sie es in einer Variablen:const button = document.getElementById('button01'); if (button) { button.addEventListener('click', newThing); }Wenn Sie JS schreiben, das für die Produktion transpiliert wird (was Sie in einem professionellen oder größeren Projekt wirklich tun sollten), verwenden Sie die optionale Verkettung:
document.getElementById('button01')?.addEventListener('click', newThing);
Das grundlegende Problem bleibt jedoch bestehen: Das HTML-Layout ist vollständig vom JavaScript getrennt. Zu überprüfen, ob überhaupt eine Schaltfläche vorhanden ist, sollte im Idealfall kein Problem sein, zumindest in einem größeren oder professionelleren Projekt. Was wäre, wenn Sie nicht 1, sondern 3 oder 5 oder 10 Elemente mit Handlern auf verschiedenen Seiten hätten, die alle existieren könnten oder nicht? Die Codebasis wäre schwieriger zu pflegen als sie sein sollte.
Hierfür gibt es einige Lösungen:
- Eine Möglichkeit besteht darin, eine separate
<script>Datei für Seiten mit dem Formular zu haben, z.
<form id="thingSelection2">
...
</form>
<script src="./thingSelection.js"></script>
wo thingSelection.jsfügt der Ereignis-Listener hinzu. Dies erfordert jedoch eine separate Anfrage an den Server, was auf einer großen Seite unter HTTP 1.1 ein Problem sein kann. Wenn Sie viele verschiedene Skripte wie dieses haben, kann die schiere Anzahl paralleler Anfragen die Arbeit verlangsamen, insbesondere für Benutzer mit hohe Latenz. (Das HTTP / 2-Protokoll hat keine Probleme mit zusätzlichen Verbindungen zum gleichen Server IIRC.)
(Sie können das Skript auch einbinden, z. B. </form><script>// etc</script>aber ich bevorzuge es, Skripte in separaten Dateien abzulegen, um das Caching zu ermöglichen.)
- Aber wenn ich es wäre, würde ich es nachdrücklich vorziehen, die Erstellung des HTML-Codes vollständig in die Ereignis-Listener für diesen HTML-Code zu integrieren, um es völlig unvorstellbar zu machen, dass einer ohne den anderen unter Verwendung eines Frameworks existiert. Mit React können Sie beispielsweise Folgendes tun:
const Things = () => {
const [item, setItem] = useState('');
const [selectedOption, setSelectedOption] = useState('foo');
const clickHandler = (e) => {
const fn = options[selectedOption];
if (fn) {
setItem(fn());
}
};
return (
<div>
<select value={selectedOption} onChange={e => setSelectedOption(e.currentTarget.value)}>
<option value="foo">foo</option>
<option value="bar">bar</option>
</select>
<button onClick={clickHandler}>click</button>
<output>{item}</output>
</div>
);
};
const options = {
foo: () => 'foo',
bar: () => 'bar',
};
const Things = () => {
const [item, setItem] = React.useState('');
const [selectedOption, setSelectedOption] = React.useState('foo');
const clickHandler = (e) => {
const fn = options[selectedOption];
if (fn) {
setItem(fn());
}
};
return (
<div>
<select value={selectedOption} onChange={e => setSelectedOption(e.currentTarget.value)}>
<option value="foo">foo</option>
<option value="bar">bar</option>
</select>
<button onClick={clickHandler}>click</button>
<output>{item}</output>
</div>
);
};
ReactDOM.render(<Things />, document.querySelector('.react'));
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div class="react"></div>
Wenn Sie sich dann auf einer Seite befinden, auf der die Dinge benötigt werden, rendern Sie einfach <Things />. Bei diesem Ansatz ist der HTML-Code der zugehörigen JS-Handler vollständig in der HTML-Datei enthalten Things. Es ist nicht erforderlich, Selektoren zu verwenden, die möglicherweise unbeabsichtigte Kollisionen aufweisen. (Wenn Sie beispielsweise Ihren ursprünglichen Code von verwenden document.getElementById('button01'), was wäre, wenn ein anderer vollständig separater Abschnitt des HTML- Codes auf einer der Seiten ein Element hätte, das versehentlich dieselbe ID verwendet hat? Dann haben Sie Probleme.)
Die Verwendung eines solchen Frameworks ist zwar viel zu lernen, macht Codebasen jedoch wesentlich wartbarer. Es lohnt sich für mittelgroße oder größere Projekte, IMO.
In einem anderen Sinne könnte Ihr aktueller Code auch ein wenig verbessert werden:
Bevorzugte Selektoren Selektorzeichenfolgen sind normalerweise leichter zu verstehen und flexibler als andere Methoden zum Auswählen von Elementen (wie getElementsByTagNameund document.forms.someFormName.someFormElement). Die Auswahlzeichenfolge, die einem Element entspricht, stimmt mit der CSS-Auswahl überein, die das Element formatiert. Zum Beispiel würde ich ersetzen:
document.forms.thingSelection2.type.value
mit
document.querySelector('#thingSelection2 [name=type]').value
und
const output = document.getElementsByTagName('output')[0];
mit
const output = document.querySelector('output');
(Sie müssen keine Sammlung auswählen, wenn Sie nur das einzelne übereinstimmende Element benötigen.)
Speichern Sie den Wert Anstatt den Wert zweimal auszuwählen und zu extrahieren, schreiben Sie DRY-Code. Setzen Sie es in eine Variable:
const { value } = document.querySelector('#thingSelection2 [name=type]');
if (value in options) { // Or, use `!options[value]
return false;
}
Es sieht so aus, als hätten Sie einige der Ratschläge aus meiner Antwort auf einen Ihrer anderen Beiträge aufgenommen - das ist schön.
Die Antwort von CertainPerformance hat gute Ratschläge. Entsprechend der Essenz gibt es andere Teile des Codes, die "problematisch " sein könnten :
const output = document.getElementsByTagName('output')[0];
Während es ein Element geben sollte , <output>wenn der HTML-Code eines hat, könnte dies zu einem Fehler führen, wenn das JavaScript ausgeführt wird, bevor das DOM bereit ist, genau wie der Code zum Abrufen eines Elements nach id- Attribut:
const output = document.getElementsByTagName('output')[0];
console.log('output tagName:', output.innerHTML)
Stellen Sie am besten sicher, dass document.getElementsByTagName('output')die Länge ungleich Null ist, bevor Sie auf das erste Element zugreifen.
Ein weiterer Vorschlag ist, so früh wie möglich zurückzukehren - z. B. hat die zuzuweisende Zeile outputkeinen Einfluss auf die Bedingung:
const output = document.getElementsByTagName('output')[0]; if (!(document.forms.thingSelection2.type.value in options)) { return false; }
Während es für diesen Code wahrscheinlich keinen Unterschied macht, ob das Ausgabeelement aus dem DOM abgerufen wird, ist es eine gute Angewohnheit, Berechnungen zu reduzieren. In anderen Situationen kann dies wertvolle Zeit für Benutzer sparen - z. B. am Beispiel einer serverseitigen Anforderung zum Abrufen von Daten, die einige Sekunden dauern kann.
Ich gehe davon aus, dass Ihr HTML ein bisschen so aussieht:
<output></output>
<output></output>
<form>
<input name="mode" value="..." />
<input name="type" value="..." />
<button>Click!</button>
</form>
Meine Kernempfehlung wäre, sich nicht auf IDs zu verlassen, da diese pro Seite eindeutig sind. Verlassen Sie sich stattdessen auf Datenattribute und gehen Sie niemals davon aus, dass es eine bestimmte Anzahl von Attributen gibt. Ihr Javascript sollte so weit wie möglich von Ihrem HTML-Layout getrennt sein.
Lassen Sie uns ändern, was Sie ein bisschen haben. Beginnen wir mit dem Hinzufügen eines Datenattributs, das JavaScript anzeigt, dass Sie eine spezielle Schaltfläche haben:
<button data-my-widget>Click!</button>
Auf diese Weise können Sie allen Schaltflächen mit dem angegebenen Attribut einen Ereignis-Listener zuweisen und Ihre Schaltflächen können schrittweise verbessert werden:
const myThing = event => {
...
}
for (const button of document.querySelectorAll('[data-my-widget]')) {
button.addEventListener('click', newThing);
}
Sie sind auch fest codiert, welche Form jede Schaltfläche für - und die jeweiligen Steuerelemente - innerhalb des Ereignishandlers gilt. Sie können dies etwas verbessern, indem Sie das Formularattribut für a verwenden buttonund die Zielelement-IDs auch als Datenattribute angeben:
<form>
<input name="mode" value="...." />
<input name="type" value="...." />
<button data-my-widget data-mode="mode" data-value="type">Click!</button>
</form>
Dann können Sie im Handler auf dieses Formular zugreifen:
const myThing = event => {
const outputs = document.getElementsByTagName('output');
if (outputs.length === 0) {
return;
}
const output = outputs[0];
const target = event.target;
const { form } = target;
if (form === undefined) {
return;
}
const { mode: modeId, value: valueId } = target.dataset;
const mode = form.elements[modeId]?.value;
const value = form.elements[valueId]?.value;
if (mode === undefined || value === undefined || value in options === false) {
return;
}
const list = options[value];
const f = list[`${mode}Item`];
if (f === undefined) {
return;
}
output.innerHTML = f();
}
Dadurch wird Ihr Javascript so wenig wie möglich über Ihre HTML-Struktur informiert, obwohl ich weiterhin empfehlen würde, den outputsAbschnitt zu ersetzen , möglicherweise durch die Verwendung von Datenattributen, anstatt sich auf den Index des Elements zu verlassen:
<output id="output-1"></output>
<button ... data-output="output-1">Click!</button>
const myThing = event => {
...
const { mode: modeId, value: valueId, output: outputId } = target.dataset;
...
const output = document.getElementById(outputId);
output.innerHTML = ...;
}
Alles zusammen:
<output id="output-1"></output>
...
<output id="output-n"></output>
<form>
<input name="mode" value="...." />
<input name="type" value="...." />
<button data-my-widget data-mode="mode" data-value="type" data-output="output-1">Click!</button>
</form>
const handleClick = event => {
const { form } = event.target;
if (form === undefined) {
return;
}
const { mode: modeId, value: valueId, output: outputId } = target.dataset;
const mode = form.elements[modeId]?.value;
const value = form.elements[valueId]?.value;
const output = document.getElementById(outputId);
if (mode === undefined || value === undefined || output === undefined) {
return;
}
const list = options[value];
const f = list[`${mode}Item`];
if (f === undefined) {
return;
}
output.innerHTML = f();
}
for (const button of document.querySelectorAll('[data-my-widget]')) {
button.addEventListener('click', handleClick);
}