Решение ошибки: Uncaught TypeError
Uncaught TypeError: document.getElementById (...) имеет значение null
У меня есть один файл JavaScript, подключенный к нескольким страницам. Приведенный ниже фрагмент - это функция, которая используется для одной страницы. Выше ошибка появляется , когда переходит пользователя к одной из страниц , которые не используют ниже функции, и она ссылается на document.getElementById('button01').addEventListener('click', newThing);линию в нижней части. Я понял, что ошибка возникает из-за того, что button01ее нет на этих страницах.
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);
Решение
Мое решение для этого просто; поместите строку в оператор if следующим образом:
if(document.getElementById('button01')) {
document.getElementById('button01').addEventListener('click', newThing);
}
Это удаляет ошибку со страниц, которые ее не используют.
Вопросов
Открывает ли это возможность для каких-либо ошибок, о которых я не знаю? Есть ли лучший способ добиться таких же результатов?
HTML
Я благодарен за отзывы, полученные на этот пост. Поскольку некоторые ответы содержат рекомендации, основанные на предположениях моего HTML, я решил добавить соответствующий HTML в приведенный ниже фрагмент. Следует отметить, что я использую две формы. Это необходимо для функциональности кода. Я не знаю ни о каких непреднамеренных ошибках, которые это может вызвать. Если да, дайте мне знать ниже.
<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>
Кроме того, я хотел бы отметить, что исходный фрагмент кода - только часть моего полного JavaScript. Основываясь на информации, представленной в этом сообщении , я пришел к выводу, что добавление оставшегося JavaScript, хотя, возможно, полезно и имеет отношение к некоторым из приведенных ниже ответов, возможно, это выходит за рамки исходной темы этого сообщения. Пожалуйста, запишите свои мысли по этому поводу в комментариях ниже.
Ответы
Открывает ли это возможность для каких-либо ошибок, о которых я не знаю?
Вряд ли. ifУтверждение в порядке, хотя это может быть чище:
Вместо того, чтобы выбирать элемент дважды (один раз для проверки, существует ли он, а в другой раз для его вызова
addEventListener), сохраните его в переменной:const button = document.getElementById('button01'); if (button) { button.addEventListener('click', newThing); }Или, если вы пишете JS, который транслируется для производства (что в профессиональном или более крупном проекте вам действительно должно быть), используйте необязательную цепочку:
document.getElementById('button01')?.addEventListener('click', newThing);
Но остается фундаментальная проблема - макет HTML полностью отключен от JavaScript. Необходимость сначала проверить, существует ли кнопка, - это то, о чем в идеале не следует беспокоиться, по крайней мере, в более крупном или более профессиональном проекте. Что, если бы у вас был не 1, а 3, 5 или 10 элементов с обработчиками на разных страницах, все из которых могут существовать, а могут и не существовать? Кодовую базу было бы сложнее поддерживать, чем следовало бы.
Для этого есть несколько решений:
- Один из вариантов - создать отдельный
<script>файл для страниц с формой, например:
<form id="thingSelection2">
...
</form>
<script src="./thingSelection.js"></script>
где thingSelection.jsдобавляет слушателя событий. Но для этого потребуется отдельный запрос к серверу, что может быть проблемой на большой странице в HTTP 1.1 - если у вас много разных сценариев, подобных этому, огромное количество параллельных запросов может замедлить работу, особенно для пользователей с высокая задержка. (Протокол HTTP / 2 не имеет проблем с дополнительными подключениями к тому же серверу IIRC)
(Вы также можете встроить сценарий, например, </form><script>// etc</script>но я предпочитаю помещать сценарии в отдельные файлы, чтобы разрешить кеширование)
- Но если бы это был я, я бы сильно предпочел полностью интегрировать создание HTML с прослушивателями событий для этого HTML, чтобы сделать совершенно немыслимым существование одного без другого, используя фреймворк. Например, с помощью React вы можете делать что-то вроде:
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>
Затем, когда вы находитесь на странице, где нужны Вещи, вы просто выполняете рендеринг <Things />. При таком подходе HTML и связанные с ним обработчики JS полностью автономны в Things. Нет необходимости использовать селекторы, которые могут иметь непреднамеренные коллизии. (Например, document.getElementById('button01')если использовать исходный код , что, если в каком-то другом полностью отдельном разделе HTML на одной из страниц есть элемент, который случайно использовал тот же идентификатор? Тогда у вас возникнут проблемы.)
По общему признанию, использование такой структуры, как эта, требует многого изучения, но она делает кодовые базы значительно более удобными в обслуживании. Это того стоит для средних или крупных проектов, ИМО.
С другой стороны, ваш текущий код также можно немного улучшить:
Предпочтительные селекторы Строки селекторов обычно более понятны и более гибки, чем другие методы выбора элементов (например, getElementsByTagNameи document.forms.someFormName.someFormElement). Строка селектора, соответствующая элементу, будет соответствовать селектору CSS, который стилизует элемент. Например, я бы заменил:
document.forms.thingSelection2.type.value
с участием
document.querySelector('#thingSelection2 [name=type]').value
и
const output = document.getElementsByTagName('output')[0];
с участием
const output = document.querySelector('output');
(нет необходимости выбирать коллекцию, если вам нужен только один соответствующий элемент)
Сохраните значение Вместо того, чтобы дважды выбирать и извлекать значение, напишите DRY-код; поместите его в переменную:
const { value } = document.querySelector('#thingSelection2 [name=type]');
if (value in options) { // Or, use `!options[value]
return false;
}
Похоже, вы включили некоторые советы из моего ответа в один из своих постов - это хорошо.
В ответе CertainPerformance есть отличный совет. Помимо сути, есть и другие части кода, которые " могут " вызвать проблемы:
const output = document.getElementsByTagName('output')[0];
Хотя должен быть <output>элемент, если он есть в HTML-коде, если JavaScript выполняется до того, как DOM будет готов, это может привести к ошибке, как и код для получения элемента по атрибуту id :
const output = document.getElementsByTagName('output')[0];
console.log('output tagName:', output.innerHTML)
Было бы лучше убедиться, что document.getElementsByTagName('output')длина не равна нулю, прежде чем обращаться к первому элементу.
Другое предложение - вернуться как можно раньше - например, строка для назначения outputне влияет на условие:
const output = document.getElementsByTagName('output')[0]; if (!(document.forms.thingSelection2.type.value in options)) { return false; }
Хотя для этого кода, вероятно, не имеет значения, извлекается ли выходной элемент из модели DOM, сокращение объема вычислений - это хорошая привычка. В других ситуациях это может сэкономить драгоценное время для пользователей - например, возьмем пример запроса на стороне сервера для выборки данных, который может занять несколько секунд.
Я предполагаю, что ваш HTML-код выглядит примерно так:
<output></output>
<output></output>
<form>
<input name="mode" value="..." />
<input name="type" value="..." />
<button>Click!</button>
</form>
Моя основная рекомендация - не полагаться на идентификаторы, поскольку они уникальны для каждой страницы. Вместо этого подумайте о том, чтобы полагаться на атрибуты данных и никогда не предполагать, что их определенное количество. Ваш javascript должен быть максимально отключен от вашего HTML-макета .
Давайте немного изменим то, что у вас есть. Начнем с добавления атрибута данных, который указывает JavaScript, что у вас есть специальная кнопка:
<button data-my-widget>Click!</button>
Это позволит вам назначить прослушиватель событий всем кнопкам с данным атрибутом и позволит вашим кнопкам оставаться прогрессивно улучшенными:
const myThing = event => {
...
}
for (const button of document.querySelectorAll('[data-my-widget]')) {
button.addEventListener('click', newThing);
}
Вы также жестко запрограммировали, что каждая кнопка применяется к - и соответствующим элементам управления - в обработчике событий. Вы можете сделать это немного лучше, используя атрибут формы в a button, а также указав идентификаторы целевых элементов в качестве атрибутов данных:
<form>
<input name="mode" value="...." />
<input name="type" value="...." />
<button data-my-widget data-mode="mode" data-value="type">Click!</button>
</form>
Затем вы можете получить доступ к этой форме в обработчике:
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();
}
Это позволяет вашему javascript как можно больше не знать о вашей структуре HTML, хотя я бы все же рекомендовал также заменить outputsраздел, возможно, с использованием атрибутов данных, а не полагаясь на индекс элемента:
<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 = ...;
}
Собираем все вместе:
<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);
}