You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

实例化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">&times;</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循环遍历数组时,会遍历字符串的每个字符索引,进而重复渲染内容。

修复步骤

  1. 修改Book构造函数:移除return this.bookInfo();语句。构造函数默认返回this(即新创建的对象实例),无需手动返回字符串。
  2. 优化遍历方式:将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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.16 07:45:59