this.myFunction is not a function error in this JavaScript class 的原因是什么?

What causes the this.myFunction is not a function error in this JavaScript class?

我正在使用普通(香草)JavaScript.

以 HTML table 的形式显示 JSON

我将从 JSON 动态创建的行附加到 table 的主体:

class CountriesList {
  constructor() {
    this.apiURL =
      "https://gist.githubusercontent.com/Goles/3196253/raw/9ca4e7e62ea5ad935bb3580dc0a07d9df033b451/CountryCodes.json";
    this.countries = [];
    this.searchBox = document.querySelector("#searchBox");
    this.stringToMatch = "";
    this.tableRows = "";
  }

  // Render rows
  renderRows = (arr, container) => {
    let el = document.querySelector(container);
    el.innerHTML = "";
    el.innerHTML += arr
      .map(function(item) {
        return `<tr>
              <td>${item.name}</td>
              <td class="text-right">${item.code}</td>
              <td class="text-right">
                <button data-code="${item.code}" class="btn btn-sm btn-danger" onclick="this.deleteRow()">
                  <i class="fa fa-times" aria-hidden="true"></i>
                </button>
              </td>
           </tr>`;
      })
      .join("");
  };

  // Get Items
  getFilteredCountries = async() => {
    const response = await fetch(this.apiURL);
    this.countries = await response.json();
    // If there is a search string, filter results
    this.stringToMatch = this.searchBox.value;
    if (this.stringToMatch.length > 0) {
      this.countries = this.countries.filter((country) => {
        return (
          country.name.toLowerCase().includes(this.stringToMatch.toLowerCase()) ||
          country.code.includes(this.stringToMatch.toUpperCase())
        );
      });
      this.renderRows(this.countries, "#countries_table tbody");
    }
  };

  deleteRow() {
    let deleteBtns = document.querySelectorAll("#countries_table tr button");
    console.log(event.target.dataset.code);
    this.countries = this.countries.filter(() => item.code != event.target.dataset.code);
    this.renderRows(this.countries, "#countries_table tbody");
  };

  hideLoader = async() => {
    let loader = document.querySelector(".loader");
    const action = this.countries.length > 0 ? "add" : "remove";
    loader.classList[action]("d-none");
  };

  init = async() => {
    await this.getFilteredCountries();
    await this.hideLoader();
    this.searchBox.addEventListener("keyup", this.getFilteredCountries);
    this.renderRows(this.countries, "#countries_table tbody");
  };
}

const countriesList = new CountriesList();
countriesList.init();
.loader {
  border: 4px solid #f3f3f3;
  border-top: 4px solid #3498db;
  opacity: .85;
  border-radius: 50%;
  width: 50px;
  height: 50px;
  position: absolute;
  top: 50px;
  left: 50%;
  margin-left: -50px;
  animation: spin 2s linear infinite;
}

@keyframes spin {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}
<link href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" />
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" rel="stylesheet" />
<div class="container-fluid">
  <div class="card bg-light shadow-sm my-2">
    <div class="card-header px-3 d-flex">
      <h6 class="text-dark m-0 align-self-center">Countries of the World</h6>
    </div>
    <div class="card-body bg-white position-relative p-0">
      <div class="search mx-2">
        <input class="my-2 form-control" id="searchBox" type="text" placeholder="Search..." value="">
      </div>
      <table class="table mb-0" id="countries_table">
        <thead>
          <tr>
            <th>Country</th>
            <th class="text-right">Code</th>
            <th class="text-right">Delete</th>
          </tr>
        </thead>
        <tbody>
        </tbody>
      </table>
      <div class="loader"></div>
    </div>
  </div>
</div>

问题

在 renderRows 方法的模板文字中,我添加了一个 删除按钮:

<button data-code="${item.code}" class="btn btn-sm btn-danger" onclick="this.deleteRow()">
   <i class="fa fa-times" aria-hidden="true"></i>
</button>

当我单击删除按钮时,我在浏览器中收到此错误:

Uncaught TypeError: this.deleteRow is not a function 

我做错了什么?

核心问题是:在您的例子中,this 关键字引用了 button 元素。一个原始的例子:

<button onclick="console.log(this)">Click me</button>

您假设,因为您在 class 中创建了按钮,所以 this 将引用 class。然而,您所做的只是创建一个包含 HTML 的字符串,然后将该字符串设置为 DOM 中某个元素的 innerHTML。从本质上讲,您最终得到的是我的回答中的摘录:只是一个按钮挂在 DOM 中的某个地方。 DOM 元素触发的事件为 this 关键字设置了元素本身。

为了实现您真正想要的,您需要在 onClick 侦听器中对 class 实例进行某种引用。一种简单的方法是让您的 class 实例作为变量全局访问,然后调用该变量的方法。这是一个简单的例子:

    class Renderer {
        render() {
            const target = document.querySelector("#app");
            target.innerHTML = `<button onClick="renderer.doSomething()">hi</button>`
        }
        doSomething() {
            console.log("I am doing something...");
        }
    }
    const renderer = new Renderer();
    renderer.render();
<div id="app"></div>

显然,此示例存在缺陷,但它应该向您展示为什么您选择的方法不起作用以及前进的方向是什么。

第二次尝试。现在一切都应该工作了。我尝试了一些不同的方法,只是因为我想了解为什么某些解决方案没有按我预期的那样工作。

据我所知这工作正常,虽然有时一个国家不会被删除并且会在控制台中输出 null 而不是国家代码,但是检查按钮会显示它的所有内容然后按钮将随机工作并删除该国家/地区...您可能需要调查一下。

希望这对您有所帮助。我在新代码所在的位置添加了一些评论;随时问我关于更改的任何问题(尽管我可能不比你知道的更多 tbh :p )

class CountriesList {
  constructor() {
    this.apiURL =
      "https://gist.githubusercontent.com/Goles/3196253/raw/9ca4e7e62ea5ad935bb3580dc0a07d9df033b451/CountryCodes.json";
    this.countries = [];
    this.searchBox = document.querySelector("#searchBox");
    this.stringToMatch = "";
    this.tableRows = "";
  }

  // A reference to the deleteRow method which we can use inside addEventListener inside renderRows
  boundDeleteRow = (event) => { this.deleteRow(event, this) }

  // Render rows
  renderRows = (arr, container) => {
    // Just added a reference to the CountriesList instance ("that")
    let el = document.querySelector(container),
        that = this;
    el.innerHTML = "";
    
    arr.forEach((item) => {
        let row = `<tr>
              <td>${item.name}</td>
              <td class="text-right">${item.code}</td>
              <td class="text-right">
                <button data-code="${item.code}" class="btn btn-sm btn-danger">
                  <i class="fa fa-times" aria-hidden="true" style="pointer-events:none;"></i>
                </button>
              </td>
           </tr>`;
         el.innerHTML += row;
    });
      
    // add event listener to every row (I used querySelectorAll because sometimes countires show up more than once in your list)
    // added "that" as the second param so I can use it to reference the CountriesList instance
    el.querySelectorAll(`button[data-code]`).forEach((i) => {
      i.addEventListener('click',that.boundDeleteRow);
    },that);
  };

  // Get Items
  getFilteredCountries = async() => {
    const response = await fetch(this.apiURL);
    this.countries = await response.json();
    // If there is a search string, filter results
    this.stringToMatch = this.searchBox.value;
    if (this.stringToMatch.length > 0) {
      this.countries = this.countries.filter((country) => {
        return (
          country.name.toLowerCase().includes(this.stringToMatch.toLowerCase()) ||
          country.code.includes(this.stringToMatch.toUpperCase())
        );
      });
      this.renderRows(this.countries, "#countries_table tbody");
    }
  };

  deleteRow(event, that) {
    let deleteBtns = document.querySelectorAll("#countries_table tr button");
    this.countries = this.countries.filter((item) => item.code != event.target.getAttribute('data-code'));
    this.renderRows(this.countries, "#countries_table tbody");
  };

  hideLoader = async() => {
    let loader = document.querySelector(".loader");
    const action = this.countries.length > 0 ? "add" : "remove";
    loader.classList[action]("d-none");
  };

  init = async() => {
    await this.getFilteredCountries();
    await this.hideLoader();
    this.searchBox.addEventListener("keyup", this.getFilteredCountries);
    this.renderRows(this.countries, "#countries_table tbody");
  };
}

const countriesList = new CountriesList();
countriesList.init();
.loader {
  border: 4px solid #f3f3f3;
  border-top: 4px solid #3498db;
  opacity: .85;
  border-radius: 50%;
  width: 50px;
  height: 50px;
  position: absolute;
  top: 50px;
  left: 50%;
  margin-left: -50px;
  animation: spin 2s linear infinite;
}

@keyframes spin {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}
<link href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" />
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" rel="stylesheet" />
<div class="container-fluid">
  <div class="card bg-light shadow-sm my-2">
    <div class="card-header px-3 d-flex">
      <h6 class="text-dark m-0 align-self-center">Countries of the World</h6>
    </div>
    <div class="card-body bg-white position-relative p-0">
      <div class="search mx-2">
        <input class="my-2 form-control" id="searchBox" type="text" placeholder="Search..." value="">
      </div>
      <table class="table mb-0" id="countries_table">
        <thead>
          <tr>
            <th>Country</th>
            <th class="text-right">Code</th>
            <th class="text-right">Delete</th>
          </tr>
        </thead>
        <tbody>
        </tbody>
      </table>
      <div class="loader"></div>
    </div>
  </div>
</div>