Решение ошибки: Uncaught TypeError

Oct 22 2020

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">&nbsp;Random
      <br/><input type="radio" name="mode" value="forward" id="mode2">&nbsp;Old&nbsp;-&nbsp;New
      <br/><input type="radio" name="mode" value="reverse" id="mode3">&nbsp;New&nbsp;-&nbsp;Old
   </form>
</div>
<div>
   <form name="thingSelection2">
       Doodle&nbsp;<input type="radio" name="type" value="doodle" id="doodleCheck"><br/>
       Video&nbsp;<input type="radio" name="type" value="video" id="videoCheck"><br/>
       Audio&nbsp;<input type="radio" name="type" value="audio" id="audioCheck"><br/>
       Photo&nbsp;<input type="radio" name="type" value="photo" id="photoCheck"><br/>
       Text&nbsp;<input type="radio" name="type" value="text" id="textCheck">
   </form>
 </div>

Кроме того, я хотел бы отметить, что исходный фрагмент кода - только часть моего полного JavaScript. Основываясь на информации, представленной в этом сообщении , я пришел к выводу, что добавление оставшегося JavaScript, хотя, возможно, полезно и имеет отношение к некоторым из приведенных ниже ответов, возможно, это выходит за рамки исходной темы этого сообщения. Пожалуйста, запишите свои мысли по этому поводу в комментариях ниже.

Ответы

10 CertainPerformance Oct 21 2020 at 23:58

Открывает ли это возможность для каких-либо ошибок, о которых я не знаю?

Вряд ли. 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;
}
4 SᴀᴍOnᴇᴌᴀ Oct 22 2020 at 05:20

Похоже, вы включили некоторые советы из моего ответа в один из своих постов - это хорошо.

В ответе 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, сокращение объема вычислений - это хорошая привычка. В других ситуациях это может сэкономить драгоценное время для пользователей - например, возьмем пример запроса на стороне сервера для выборки данных, который может занять несколько секунд.

3 Dan Oct 22 2020 at 18:57

Я предполагаю, что ваш 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);
}