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

Vanilla JS图书馆项目:移除书籍与阅读状态切换功能故障

图书馆项目移除书籍与切换阅读状态功能故障排查

我使用Vanilla JS与HTML开发一个图书馆项目,整体功能已基本完成,但移除书籍(removeBook)和切换阅读状态(toggleRead)两个函数无法正常工作:

  • toggleRead函数执行book.isRead = !book.isRead时触发TypeError
  • removeBook函数完全无法实现移除功能

有问题的函数代码片段

const removeBook = (e) => {
    const title = e.currentTarget.innerHTML.replaceAll(
        '"',
        '',
        'Title: ')
    library.removeBook(title)
    updateLibContainer()
    console.log('remove');
}
const toggleRead = (e) => {
    const title = e.currentTarget.innerHTML.replaceAll(
        '"',
        '',
        'Title: ')
    const book = library.getTitle(title)
    book.isRead = !book.isRead
    console.log('read');
    updateLibContainer()
}

项目完整代码

JavaScript代码

// Selectors for pop up and closing pop up when x is pressed
const popUp = document.querySelector('.pop-up');
const closePopUp = document.getElementsByTagName('span')[0];
closePopUp.addEventListener('click', () => popUp.style.display = 'none')

const newBookBtn = document.querySelector('.newbtn');
newBookBtn.addEventListener('click', () => popUp.style.display = 'block')


// Constructor for books
class Book {
  constructor(
    title = 'Unkown',
    author = 'Unkown',
    pages = 0,
    isRead = false
  ) {
    this.title = title
    this.author = author
    this.pages = pages
    this.isRead = isRead
  }
}

// Constructor for Library
class Library {
  constructor() {
    this.books = []
  }
  addBook(newBook) {
    this.books.push(newBook)
  }
  removeBook(title) {
    this.books = this.books.filter((book) => book.title !== title)
  }
  getTitle(title) {
    return this.books.find(book => book.title === title)
  }

  isInLibrary(title) {
    return this.books.some(book => book.title === title)
  }
}
const library = new Library()

// Additional UI declarations
const LibContainer = document.getElementById('lib-container')
const addBookForm = document.getElementById('add-form')

// Handling and creating book from form input data
const bookFromInput = () => {
  const title = document.getElementById('title').value
  const author = document.getElementById('author').value
  const pages = document.getElementById('pages').value
  const isRead = document.getElementById('is-read').checked

  return new Book(title, author, pages, isRead)

}
// Function when form is submitted to create new book from input, check if it exists
// and close form
const addNewBook = (e) => {
  e.preventDefault()
  const newBook = bookFromInput()
  if (library.isInLibrary(newBook.title)) {
    alert('Book already exists')
  } else {
    library.addBook(newBook)
    exitForm()
    updateLibContainer()
  }
}
const exitForm = () => {
  addBookForm.reset()
  popUp.style.display = 'none'
}
const resetLibContainer = () => {
  LibContainer.innerHTML = ''
  console.log('reset Container');
}
const updateLibContainer = () => {
  resetLibContainer()
  for (let book of library.books) {
    createBookCard(book)
  }
  console.log('updateContainer');
}


// Creating book card 
const createBookCard = (book) => {
  const bookCard = document.createElement('div')
  const title = document.createElement('p')
  const author = document.createElement('p')
  const pages = document.createElement('p')
  const changeReadStatus = document.createElement('button')
  const removeBookBttn = document.createElement('button')

  bookCard.classList.add('book-card')
  changeReadStatus.classList.add('change-read-status')
  changeReadStatus.onclick = toggleRead
  removeBookBttn.classList.add('remove-book')
  removeBookBttn.onclick = removeBook

  title.textContent = `Title: ${book.title}`
  author.textContent = `Author: ${book.author}`
  pages.textContent = `Pages: ${book.pages}`
  removeBookBttn.textContent = 'Remove'

  if (book.isRead) {
    changeReadStatus.textContent = 'Read'
    changeReadStatus.style.backgroundColor = '#68f364'
  } else {
    changeReadStatus.textContent = 'Not read'
    changeReadStatus.style.backgroundColor = '#d16767'
  }


  bookCard.appendChild(title)
  bookCard.appendChild(author)
  bookCard.appendChild(pages)
  bookCard.appendChild(changeReadStatus)
  bookCard.appendChild(removeBookBttn)
  LibContainer.appendChild(bookCard)
}

const removeBook = (e) => {

  const title = e.currentTarget.innerHTML.replaceAll(
    '"',
    '',
    'Title: ')
  library.removeBook(title)
  updateLibContainer()
  console.log('remove');
}
const toggleRead = (e) => {
  const title = e.currentTarget.innerHTML.replaceAll(
    '"',
    '',
    'Title: ')
  const book = library.getTitle(title)
  book.isRead = !book.isRead
  console.log('read');
  updateLibContainer()
}

addBookForm.onsubmit = addNewBook

HTML代码

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Library</title>
  <link rel="stylesheet" href="style.css">
</head>

<body>
  <div class="header">
    <h1>The Library</h1>
  </div>

  <div class="newbtn-container">
    <button class="newbtn">+ New Book</button>
  </div>
  <div class="pop-up">
    <form class="form" action id="add-form">
      <span>x</span>
      <legend>New book</legend>
      <input type="text" id="title" class="form" placeholder="Title">
      <input type="text" id="author" class="form" placeholder="Author">
      <input type="text" id="pages" class="form" placeholder="Pages">

      <div class="checkbox">
        <label>Read: </label>
        <input type="checkbox" class="form" id="is-read">
      </div>
      <button class="form" id="submit">Add</button>
    </form>
  </div>
  <div id="lib-container">
    <div class="book-card">
      <p>Title: 22</p>
      <p>Author: 22</p>
      <p>Pages: 22</p>
      <button class="change-read-status" style="background-color: rgb(209, 103, 103);">Not read</button>
      <button class="remove-book">Remove</button>
    </div>
  </div>

</body>
<script src="script.js"></script>

</html>

问题根源

e.currentTarget指向的是触发事件的按钮(移除按钮的innerHTML是"Remove",状态切换按钮的innerHTML是"Read"或"Not read"),而非书籍标题元素。你试图通过replaceAll从按钮的文本中提取标题,这完全无法得到正确的书籍标题,导致:

  • library.getTitle(title)返回undefined,执行book.isRead = !book.isRead时触发TypeError
  • library.removeBook(title)找不到对应书籍,所以无法移除

修复方案

方案一:使用自定义属性存储书籍标题

修改createBookCard函数,给两个按钮添加data-title属性存储书籍标题:

const createBookCard = (book) => {
  // ... 其他代码不变
  changeReadStatus.classList.add('change-read-status')
  changeReadStatus.onclick = toggleRead
  // 添加自定义属性
  changeReadStatus.setAttribute('data-title', book.title)
  
  removeBookBttn.classList.add('remove-book')
  removeBookBttn.onclick = removeBook
  // 添加自定义属性
  removeBookBttn.setAttribute('data-title', book.title)
  // ... 其他代码不变
}

然后修改removeBook和toggleRead函数,从自定义属性中获取标题:

const removeBook = (e) => {
    const title = e.currentTarget.dataset.title;
    library.removeBook(title);
    updateLibContainer();
    console.log('remove');
}

const toggleRead = (e) => {
    const title = e.currentTarget.dataset.title;
    const book = library.getTitle(title);
    // 增加判断避免找不到书籍时报错
    if (book) {
        book.isRead = !book.isRead;
        updateLibContainer();
        console.log('read');
    }
}

方案二:通过DOM遍历获取标题

无需修改卡片创建逻辑,直接从按钮所在的卡片中找到标题元素:

const removeBook = (e) => {
    // 找到按钮所在的书籍卡片
    const bookCard = e.currentTarget.closest('.book-card');
    // 获取标题元素的文本内容
    const titleText = bookCard.querySelector('p').textContent;
    // 提取纯标题文本
    const title = titleText.replace('Title: ', '');
    library.removeBook(title);
    updateLibContainer();
    console.log('remove');
}

const toggleRead = (e) => {
    const bookCard = e.currentTarget.closest('.book-card');
    const titleText = bookCard.querySelector('p').textContent;
    const title = titleText.replace('Title: ', '');
    const book = library.getTitle(title);
    if (book) {
        book.isRead = !book.isRead;
        updateLibContainer();
        console.log('read');
    }
}

两种方案都能解决问题,方案一更简洁高效,方案二无需修改卡片创建逻辑,可根据需求选择。

内容的提问来源于stack exchange,提问作者Sinned

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 14:27:02