Giải pháp lỗi: Uncaught TypeError
Uncaught TypeError: document.getElementById (...) là rỗng
Tôi có một tệp JavaScript duy nhất được kết nối với nhiều trang. Đoạn mã dưới đây là một hàm được sử dụng cho một trang. Các trên báo lỗi xuất hiện khi điều hướng người dùng đến một trong các trang mà không sử dụng dưới hàm, và nó tham chiếu đến document.getElementById('button01').addEventListener('click', newThing);dòng ở phía dưới. Tôi đã thu thập được rằng lỗi xuất hiện do thực tế button01không tồn tại trên các trang này.
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);
Giải pháp
Giải pháp của tôi cho điều này là đơn giản; đặt dòng trong câu lệnh if như sau:
if(document.getElementById('button01')) {
document.getElementById('button01').addEventListener('click', newThing);
}
Thao tác này sẽ xóa lỗi khỏi các trang không sử dụng nó.
Câu hỏi
Điều này có mở ra khả năng xảy ra bất kỳ hành vi lỗi nào mà tôi không biết không? Có cách nào tốt hơn để đạt được kết quả tương tự?
HTML
Tôi cảm ơn vì những phản hồi mà bài đăng này đã nhận được cho đến nay. Vì một số câu trả lời chứa các đề xuất dựa trên các giả định về HTML của tôi, tôi đã quyết định thêm HTML tương ứng trong đoạn mã bên dưới. Một điều cần lưu ý là tôi sử dụng hai hình thức. Điều này là bắt buộc đối với chức năng của mã. Tôi không biết về bất kỳ lỗi ngoài ý muốn nào mà điều này có thể gây ra. Nếu có, vui lòng cho tôi biết bên dưới.
<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>
Hơn nữa, tôi muốn chỉ ra rằng đoạn mã gốc chỉ là một phần trong JavaScript đầy đủ của tôi. Dựa trên thông tin được cung cấp bởi bài đăng này , tôi đã kết luận rằng việc thêm JavaScript còn lại, mặc dù có thể có lợi và có liên quan đến một số câu trả lời bên dưới, có thể nằm ngoài phạm vi chủ đề ban đầu của bài đăng này. Hãy ghi lại suy nghĩ của bạn về điều này trong phần bình luận bên dưới.
Trả lời
Điều này có mở ra khả năng xảy ra bất kỳ hành vi lỗi nào mà tôi không biết không?
Nó không có khả năng. Các iftuyên bố là tốt, mặc dù nó có thể được thực hiện sạch hơn:
Thay vì chọn phần tử hai lần (một lần để kiểm tra xem nó có tồn tại hay không, lần khác để gọi phần tử
addEventListenerđó), hãy lưu phần tử đó trong một biến:const button = document.getElementById('button01'); if (button) { button.addEventListener('click', newThing); }Hoặc, nếu bạn đang viết JS được chuyển đổi để sản xuất (trong một dự án chuyên nghiệp hoặc lớn hơn, bạn thực sự nên làm như vậy), hãy sử dụng chuỗi tùy chọn:
document.getElementById('button01')?.addEventListener('click', newThing);
Nhưng vấn đề cơ bản vẫn còn - bố cục HTML hoàn toàn bị ngắt kết nối với JavaScript. Việc phải kiểm tra xem liệu một nút có tồn tại ngay từ đầu hay không là điều lý tưởng không phải là vấn đề đáng lo ngại, ít nhất là trong một dự án lớn hơn hoặc chuyên nghiệp hơn. Điều gì sẽ xảy ra nếu bạn không có 1, mà là 3 hoặc 5 hoặc 10 phần tử với các trình xử lý trên các trang khác nhau, tất cả đều có thể tồn tại hoặc không? Cơ sở mã sẽ khó duy trì hơn mức cần thiết.
Có một số giải pháp cho việc này:
- Một tùy chọn là có một
<script>tệp riêng cho các trang có biểu mẫu, ví dụ:
<form id="thingSelection2">
...
</form>
<script src="./thingSelection.js"></script>
nơi thingSelection.jsthêm trình nghe sự kiện. Nhưng điều đó sẽ yêu cầu một yêu cầu riêng đến máy chủ, đây có thể là sự cố trên một trang lớn trên HTTP 1.1 - nếu bạn có nhiều tập lệnh khác nhau như thế này, số lượng yêu cầu song song tuyệt đối có thể làm chậm mọi thứ, đặc biệt là đối với người dùng độ trễ cao. (Giao thức HTTP / 2 không có vấn đề với các kết nối bổ sung đến cùng một máy chủ IIRC)
(Bạn cũng có thể nội dòng tập lệnh, ví dụ: </form><script>// etc</script>nhưng tôi thích đặt các tập lệnh trong các tệp riêng biệt để cho phép lưu vào bộ nhớ đệm)
- Nhưng nếu là tôi, tôi thực sự muốn tích hợp đầy đủ việc tạo HTML với các trình xử lý sự kiện cho HTML đó, để làm cho nó hoàn toàn không thể tưởng tượng được về cái này mà không có cái kia, bằng cách sử dụng một khuôn khổ. Ví dụ, với React, bạn có thể thực hiện một số việc như sau:
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>
Sau đó, khi bạn đang ở một trang có Nội dung cần thiết, bạn chỉ cần hiển thị <Things />. Với cách tiếp cận này, HTML các trình xử lý JS liên quan của nó đều hoàn toàn độc lập trong Things. Không cần sử dụng các bộ chọn có thể có va chạm không chủ ý. (Ví dụ: đi với mã gốc của bạn document.getElementById('button01'), điều gì sẽ xảy ra nếu một số phần hoàn toàn riêng biệt khác của HTML trên một trong các trang có một phần tử vô tình sử dụng cùng một ID? Sau đó, bạn sẽ gặp sự cố.)
Sử dụng một khuôn khổ như thế này phải học rất nhiều điều, nhưng nó làm cho các cơ sở mã dễ bảo trì hơn đáng kể. Nó rất xứng đáng cho các dự án quy mô trung bình hoặc lớn hơn, IMO.
Một lưu ý khác, mã hiện tại của bạn cũng có thể được cải thiện một chút:
Bộ chọn ưu tiên Các chuỗi bộ chọn thường dễ hiểu hơn và linh hoạt hơn các phương pháp chọn phần tử khác (như getElementsByTagNamevà document.forms.someFormName.someFormElement). Chuỗi bộ chọn khớp với một phần tử sẽ phù hợp với bộ chọn CSS tạo kiểu cho phần tử đó. Ví dụ, tôi sẽ thay thế:
document.forms.thingSelection2.type.value
với
document.querySelector('#thingSelection2 [name=type]').value
và
const output = document.getElementsByTagName('output')[0];
với
const output = document.querySelector('output');
(không cần chọn tập hợp khi bạn chỉ cần phần tử phù hợp duy nhất)
Lưu giá trị Thay vì chọn và trích xuất giá trị hai lần, hãy viết mã DRY; đặt nó vào một biến:
const { value } = document.querySelector('#thingSelection2 [name=type]');
if (value in options) { // Or, use `!options[value]
return false;
}
Có vẻ như bạn đã kết hợp một số lời khuyên từ câu trả lời của tôi vào một trong những bài viết khác của bạn - điều đó thật tuyệt.
Câu trả lời của MostPerformance có lời khuyên tuyệt vời. Cùng với bản chất của nó, có những phần khác của mã " có thể " có vấn đề:
const output = document.getElementsByTagName('output')[0];
Mặc dù phải có một <output>phần tử nếu mã HTML có một phần tử, nhưng nếu JavaScript thực thi trước khi DOM sẵn sàng thì điều này có thể dẫn đến lỗi giống như mã để lấy một phần tử theo thuộc tính id sẽ:
const output = document.getElementsByTagName('output')[0];
console.log('output tagName:', output.innerHTML)
Tốt nhất là đảm bảo rằng document.getElementsByTagName('output')có độ dài khác 0 trước khi truy cập phần tử đầu tiên.
Một gợi ý khác là quay lại càng sớm càng tốt - ví dụ: dòng để gán outputkhông ảnh hưởng đến điều kiện:
const output = document.getElementsByTagName('output')[0]; if (!(document.forms.thingSelection2.type.value in options)) { return false; }
Mặc dù nó có thể không tạo ra sự khác biệt nào đối với mã này cho dù phần tử đầu ra có được tìm nạp từ DOM hay không, nhưng việc giảm bớt tính toán là một thói quen tốt để phát triển. Trong các tình huống khác, nó có thể tiết kiệm thời gian quý báu cho người dùng - ví dụ: lấy ví dụ về yêu cầu phía máy chủ để tìm nạp dữ liệu, quá trình này có thể mất vài giây.
Tôi cho rằng HTML của bạn trông giống như sau:
<output></output>
<output></output>
<form>
<input name="mode" value="..." />
<input name="type" value="..." />
<button>Click!</button>
</form>
Đề xuất cốt lõi của tôi sẽ không dựa vào ID vì chúng là duy nhất trên mỗi trang. Thay vào đó, hãy xem xét việc dựa vào các thuộc tính dữ liệu và không bao giờ giả định rằng có một số lượng cụ thể trong số đó. Javascript của bạn phải được ngắt kết nối khỏi bố cục HTML của bạn càng nhiều càng tốt.
Hãy sửa đổi những gì bạn có một chút. Hãy bắt đầu với việc thêm một thuộc tính dữ liệu cho JavaScript biết rằng bạn có một nút đặc biệt:
<button data-my-widget>Click!</button>
Điều này sẽ cho phép bạn chỉ định một trình nghe sự kiện cho tất cả các nút có thuộc tính đã cho và sẽ cho phép các nút của bạn tiếp tục được nâng cao:
const myThing = event => {
...
}
for (const button of document.querySelectorAll('[data-my-widget]')) {
button.addEventListener('click', newThing);
}
Bạn cũng đang mã hóa cứng mà hình thức mỗi nút áp dụng - và các điều khiển tương ứng - trong trình xử lý sự kiện. Bạn có thể làm cho điều này tốt hơn một chút bằng cách sử dụng thuộc tính biểu mẫu trên a button, và bằng cách cung cấp id phần tử đích làm thuộc tính dữ liệu.:
<form>
<input name="mode" value="...." />
<input name="type" value="...." />
<button data-my-widget data-mode="mode" data-value="type">Click!</button>
</form>
Sau đó, bạn có thể truy cập biểu mẫu này trong trình xử lý:
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();
}
Điều này giữ cho javascript của bạn không biết về cấu trúc HTML của bạn càng tốt, mặc dù tôi vẫn khuyên bạn nên thay thế outputsphần này, có thể bằng cách sử dụng các thuộc tính dữ liệu, thay vì dựa vào chỉ mục của phần tử:
<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 = ...;
}
Kết hợp tất cả những điều này với nhau:
<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);
}