实例化Book构造函数时页面重复渲染已有图书项问题求助
图书列表重复渲染问题
我创建了名为Book的构造函数,以及用于存储图书对象实例的空数组myLibrary。另有一个创建图书的函数createBook会调用addBookToLibrary函数,将图书推入myLibrary数组,清空添加图书的表单,再通过viewBookList函数渲染图书列表。
每次调用createBook创建Book的新实例时,页面渲染的图书列表会重复显示新增图书之前创建的所有Book实例,再加上新图书。例如,我之前创建了book1、book2和book3,之后添加book4,此时数组正确包含[ {book1...}, {book2...}, {book3...}, {book4...} ]对象,但页面上却混乱地重复显示book1、book1、book2、book1、book2、book3、book1、book2、book3、book4,控制台中的数组是正常的。

我最初在createBook中清空myLibrary数组时,页面能正确渲染图书,但显然这会清空数组,而我之后需要从数组中删除图书,所以这种方法不可行。请问如何每次正确显示数组中的内容,而不重复渲染已添加的图书?
完整代码
JavaScript
const submitButton = document.querySelector('#submit'); const btnOpenModal = document.querySelector('.open-modal'); const btnCloseModal = document.querySelector('.close-modal'); const modalElement = document.querySelector('.modal'); let myLibrary = []; let count = myLibrary.length; // Constructor... function Book(title, author, pages, read) { this.title = title; this.author = author; this.pages = Number(pages); this.read = Boolean(read); // Methods this.bookInfo = function () { const wasRead = this.read === true ? 'read' : 'not read'; return `${this.title} written by ${this.author}, ${this.pages} pages in length was ${wasRead}.`; }; return this.bookInfo(); } function createBook(e) { e.preventDefault(); let title = document.querySelector('#title').value; let author = document.querySelector('#author').value; let pages = document.querySelector('#pages').value; let read = document.querySelector('#read').value; // Instantiate new Book object const newBook = new Book(title, author, pages, read); addBookToLibrary(newBook); clearForm(); viewBookList(myLibrary); } function addBookToLibrary(book) { removeClass(); return myLibrary.push(book); } function clearForm() { document.querySelector('#form').reset(); } function setCardStyle(element, details) { element.setAttribute( 'style', 'display: flex; flex-direction: column; justify-content: space-between; text-align: center; background-color: #fff; padding: 1em; margin: 1em 1em 1em 0; border-radius: 5px; height: 250px; width: 250px; line-height: 1.5; box-shadow: 0 5px 10px 2px rgba(0, 0, 0, 0.2);', ); element.innerHTML = ` <h3 class="title">${details.title}</h3> <br> <hr> <br> <p>${details.author}</p> <p>${details.pages} pages</p> <p>${details.read === true ? 'Read' : 'Unread'}</p>`; } function createCard() { let bookCard = document.createElement('div'); bookCard.classList.add('card'); bookCard.setAttribute('data-target', `${count++}`); return bookCard; } function removeBookBtn() { let btn = document.createElement('button'); btn.setAttribute( 'style', 'color: red; height: 2.5em; width: 50%; border-radius: 5px; margin: 0 auto; font-weight: bold; text-transform: uppercase; cursor: pointer;', ); btn.innerHTML = 'Delete'; return btn; } function handleDelete(e) { let bookIndex = parseInt(e.path[1].attributes[1].value); myLibrary.splice(bookIndex, 1); return viewBookList(myLibrary); } function clearDOM(element) { while (element.firstElementChild) { element.firstElementChild.remove(); } } function viewBookList(list) { const bookDiv = document.querySelector('.book-list'); clearDOM(bookDiv); for (book in list) { let bookDetails = list[book]; let renderCard = createCard(); const deleteButton = removeBookBtn(); deleteButton.addEventListener('click', handleDelete); setCardStyle(renderCard, bookDetails); renderCard.appendChild(deleteButton); bookDiv.appendChild(renderCard); } return bookDiv; } function addClass() { return modalElement.classList.add('open'); } function removeClass() { return modalElement.classList.remove('open'); } // Event listeners btnOpenModal.addEventListener('click', addClass); btnCloseModal.addEventListener('click', removeClass); submitButton.addEventListener('click', createBook);
CSS
@import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap'); :root { --roboto: 'Roboto', sans-serif; --pt: "PT Sans", sans-serif; } * { box-sizing: border-box; margin: 0; padding: 0; } html { font-family: var(--roboto); } body { min-height: 100vh; display: flex; justify-content: center; background-image: linear-gradient(to top, #a6c1ee 0%, #fbc2eb 100%); } .header { text-align: center; } .header h1 { color: #fff; font-size: 4rem; text-shadow: 1px 2px 5px #000; } .modal { padding: 1em; background-color: #fff; } .label { text-transform: capitalize; } .btn { font-family: "PT Sans", sans-serif; font-size: 1.2rem; padding: 1rem 2.5rem; cursor: pointer; border: none; } .modal { display: none; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; align-items: center; justify-content: center; background: rgba(0, 0, 0, 0.7); } .open-modal { background: #fff; border-radius: 5px; box-shadow: 0 5px 10px 2px rgba(0, 0, 0, 0.2); transition: all 0.2s linear; } .open-modal:hover { box-shadow: none; background: rgba(255, 255, 255, 0.8); } .modal.open { display: flex; } .form { padding: 5em; } .modal-container { background-color: #fff; width: 90%; max-width: 450px; position: relative; } .modal.open .modal-container { animation: move 0.6s ease-out; } @keyframes move { from { transform: translateY(-50px); } to { transform: translateY(0px); } } .close-modal { font-size: 3rem; position: absolute; top: 0; right: 0; background-color: #fff; height: 40px; width: 40px; text-align: center; line-height: 40px; cursor: pointer; transition: color 0.15s linear; } .close-modal:hover { color: #f00; } .book-list { display: flex; flex-wrap: wrap; margin-top: 1.5em; gap: 2em; }
HTML
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="styles/style.css"> <script src="scripts/app.js" defer></script> <title>Odin Library</title> </head> <body> <div class="container"> <header class="header"> <h1>Library</h1> </header> <section class="modal open"> <div class="modal-container"> <span id="close-modal" class="close-modal">×</span> <form action="" method="POST" name="bookForm" id="form" class="form"> <div class="form-row"> <label for="title" class="label">title: </label> <input type="text" name="title" id="title" placeholder="Animal Farm" required> </div> <div class="form-row"> <label for="author" class="label">author: </label> <input type="text" name="author" id="author" placeholder="George Orwell" required> </div> <div class="form-row"> <label for="pages" class="label">pages: </label> <input type="number" name="pages" id="pages" placeholder="232" required> </div> <div class="form-row"> <label for="read" class="label">read: </label> <input type="checkbox" name="read" id="read" required> </div> <div class="button-container"> <input type="submit" value="Create" id="submit" class="submit"> </div> </form> </div> </section class="card-section"> <section> <button class="btn open-modal" id="open-modal">Add Book</button> <div class="book-list"> </div> </section> </div> </body> </html>
问题原因与解决方法
核心问题
你的Book构造函数手动返回了字符串(return this.bookInfo();),导致myLibrary数组中存储的不是Book对象实例,而是bookInfo方法返回的字符串。当使用for...in循环遍历数组时,会遍历字符串的每个字符索引,进而重复渲染内容。
修复步骤
- 修改
Book构造函数:移除return this.bookInfo();语句。构造函数默认返回this(即新创建的对象实例),无需手动返回字符串。 - 优化遍历方式:将
viewBookList中的for...in循环改为数组专用的forEach或for...of,避免遍历数组的额外属性。
修正后的关键代码
修正Book构造函数
function Book(title, author, pages, read) { this.title = title; this.author = author; this.pages = Number(pages); this.read = Boolean(read); // Methods this.bookInfo = function () { const wasRead = this.read === true ? 'read' : 'not read'; return `${this.title} written by ${this.author}, ${this.pages} pages in length was ${wasRead}.`; }; // 移除手动返回的字符串 // return this.bookInfo(); }
优化viewBookList遍历
function viewBookList(list) { const bookDiv = document.querySelector('.book-list'); clearDOM(bookDiv); // 改用forEach遍历数组元素 list.forEach((bookDetails) => { let renderCard = createCard(); const deleteButton = removeBookBtn(); deleteButton.addEventListener('click', handleDelete); setCardStyle(renderCard, bookDetails); renderCard.appendChild(deleteButton); bookDiv.appendChild(renderCard); }); return bookDiv; }
额外优化建议
- 修复删除逻辑:当前通过
data-target获取索引的方式不可靠,删除元素后count不会重置,新元素的data-target会与数组索引脱节。建议给每个Book实例添加唯一ID,或在创建卡片时直接绑定对应数组索引。 - 分离样式:将内联样式移至CSS文件,提升代码可维护性。
内容的提问来源于stack exchange,提问作者JackJack
相关产品推荐
相关产品推荐

