JavaScript教程:从基础语法到第一个交互页面

AI教程5小时前发布 jdkjadmin
0 00
JavaScript教程:从基础语法到第一个交互页面

JavaScript教程:从基础语法到第一个交互页面

JavaScript(简称 JS)是一种常用的编程语言。在网页中,它可以响应点击、修改页面内容、校验表单、请求服务器数据;在浏览器之外,JavaScript 也可以运行在 Node.js 等环境中,用于命令行工具、服务端应用和自动化任务。

本文从浏览器中的 JavaScript 入手,介绍基础语法、数据结构、函数、DOM 操作与事件处理,最后制作一个可以直接运行的待办清单。示例使用原生 JavaScript,不依赖框架或构建工具。

一、JavaScript 适合做什么

在网页开发中,HTML 负责内容结构,CSS 负责外观,JavaScript 负责行为。例如,按钮点击后切换菜单、输入内容后即时提示、从服务器加载列表,都可以由 JavaScript 完成。

  • 浏览器交互:响应点击、键盘输入、表单提交和滚动等事件。
  • 页面更新:根据数据创建、修改或删除页面元素。
  • 网络通信:使用 fetch 等接口请求服务器,并处理返回的数据。
  • 非浏览器程序:借助 Node.js 等运行时编写服务端程序、脚本和工具。

JavaScript 在不同运行环境中可用的接口并不相同。浏览器提供 document、window 等网页 API;Node.js 提供文件系统等服务端 API。语言语法有共通之处,但不能假设某个环境里的全局对象在另一个环境中也存在。

二、准备运行环境

最简单的方式是创建一个 .html 文件,用浏览器打开,再通过开发者工具查看输出和错误。浏览器开发者工具通常可以按 F12 打开;不同系统和浏览器的快捷键略有差别。

下面这个页面可以直接保存为 hello.html 后打开:

<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>JavaScript 入门</title>
</head>
<body>
  <h1>打开开发者工具查看输出</h1>
  <script>
    console.log("你好,JavaScript!");
  </script>
</body>
</html>

也可以把脚本放到独立的 .js 文件中,并通过 <script src="..."> 引入。对于多数页面脚本,推荐在脚本标签上使用 defer,让浏览器先解析 HTML,再执行脚本:

<script src="app.js" defer></script>

使用 defer 的外部经典脚本会在文档解析完成后执行,并按文档顺序执行。它不适用于内联脚本;若使用 ES 模块,模块脚本本身也采用延迟执行行为。初学时可以先把脚本放在 </body> 前,或使用上面的 defer 写法,避免脚本运行时页面元素尚未创建。

三、变量、常量与基本类型

变量用于保存数据。现代 JavaScript 通常使用 const 和 let;旧式的 var 有不同的作用域规则,新代码一般不需要使用它。

const siteName = "学习笔记";
let count = 0;

count = count + 1;
console.log(siteName, count);

const 表示变量绑定不能重新赋值,不代表它引用的对象内容绝对不能改变。比如下面的对象仍然可以修改属性:

const user = { name: "小林" };
user.name = "小周"; // 可以:修改对象属性
// user = {};       // 不可以:重新给 const 变量赋值

常见的原始类型包括字符串、数字、布尔值、undefined、null 和 BigInt;对象、数组和函数则属于对象类型。可用 typeof 查看部分值的类型,但它并不能准确区分所有类型:

console.log(typeof "文字");   // "string"
console.log(typeof 12);       // "number"
console.log(typeof true);     // "boolean"
console.log(typeof undefined);// "undefined"
console.log(typeof null);     // "object",这是语言长期保留的历史行为
console.log(Array.isArray([])); // true,用它判断数组

数字类型包括整数和小数,且存在 NaN、Infinity 等特殊值。浮点数运算可能出现精度误差,例如 0.1 + 0.2 并不严格等于 0.3。需要精确金额计算时,通常以最小货币单位的整数保存,或选用经过审查的精确计算方案,不要依赖浮点数直接做财务精算。

四、表达式、比较与条件判断

表达式可以计算出一个值。常见算术运算符有 +、-、*、/ 和 %。当 + 的操作数中包含字符串时,它可能用于拼接,而不是数值相加;在处理表单输入时,应明确转换类型。

const inputValue = "8";
const amount = Number(inputValue);

if (Number.isFinite(amount)) {
  console.log(amount + 2); // 10
}

比较值时,优先使用严格相等 === 和严格不等 !==。宽松相等 == 会执行类型转换,容易产生不直观的结果:

console.log(5 === "5"); // false
console.log(5 == "5");  // true,发生了类型转换

条件语句根据布尔条件选择执行路径。下面的例子使用 if 判断分数,并用逻辑运算符组合条件:

const score = 82;

if (score >= 90) {
  console.log("优秀");
} else if (score >= 60) {
  console.log("通过");
} else {
  console.log("继续练习");
}

const hasName = true;
const hasPermission = false;

if (hasName && hasPermission) {
  console.log("可以继续");
}

常见的“假值”包括 false、0、-0、空字符串、null、undefined 和 NaN。其他值通常为真值,包括空数组和空对象。空数组并不会因为“没有元素”而自动变成假值:

if ([]) {
  console.log("这段会执行");
}

空值合并运算符 ?? 只在左侧是 null 或 undefined 时使用默认值;逻辑或 || 则会把 0 和空字符串也视为需要替换的值:

const count = 0;

console.log(count || 10); // 10
console.log(count ?? 10);  // 0

五、循环、数组与对象

循环用于重复执行代码。遍历数组时,常用 for...of,或使用数组的 forEach、map、filter 等方法。map 适合把每一项转换成新数组,filter 适合筛选符合条件的项。

const numbers = [2, 5, 8];

for (const number of numbers) {
  console.log(number);
}

const doubled = numbers.map(function (number) {
  return number * 2;
});

const largeNumbers = numbers.filter(function (number) {
  return number > 4;
});

console.log(doubled);     // [4, 10, 16]
console.log(largeNumbers);// [5, 8]

对象用键值对表示相关数据,适合描述一个实体;数组适合保存有顺序的一组数据:

const task = {
  id: 1,
  text: "阅读文档",
  done: false
};

console.log(task.text);

const tasks = [
  task,
  { id: 2, text: "练习代码", done: true }
];

console.log(tasks.length);

访问对象属性可使用点号或方括号。属性名需要动态决定、或包含特殊字符时使用方括号。访问可能不存在的嵌套属性时,可以使用可选链 ?.,避免因中间值为空而抛出错误:

const profile = { contact: null };

console.log(profile.contact?.email); // undefined

对象与数组赋值时,通常复制的是引用,而不是自动深拷贝。修改一个变量指向的对象,另一个引用可能观察到同样的变化。新手在修改共享数据时应留意这一点;展开语法可以复制一层,但嵌套对象仍可能共享引用。

六、函数:组织和复用逻辑

函数把一段操作封装起来,可接收参数并返回结果。参数是函数调用时传入的输入,return 用于返回结果;如果没有写 return,函数结果是 undefined。

function add(a, b) {
  return a + b;
}

console.log(add(3, 4)); // 7

也可以使用函数表达式或箭头函数。箭头函数常用于简短回调;当函数需要自己的 this 语义,或涉及构造调用时,不应机械地把普通函数改写成箭头函数。

const multiply = (a, b) => a * b;
console.log(multiply(3, 4)); // 12

参数不会自动获得可靠的类型限制。若函数只接受特定范围的值,应在函数内部验证,或在调用边界进行验证。以下例子只接受非负有限数字:

function formatPrice(value) {
  if (typeof value !== "number" ||
      !Number.isFinite(value) ||
      value < 0) {
    throw new TypeError("价格必须是非负有限数字");
  }

  return value.toFixed(2);
}

console.log(formatPrice(12.5)); // "12.50"

这类校验能避免普通程序错误,但客户端检查不能代替服务端校验。用户可以修改浏览器中的代码和请求内容;凡是涉及权限、账户、价格或数据完整性的规则,服务端必须独立验证。

七、DOM:让 JavaScript 操作页面

DOM(文档对象模型)是浏览器把 HTML 文档表示为对象结构后提供的接口。JavaScript 可以通过选择器找到元素,再读取或修改其属性、文本与内容。

const title = document.querySelector("h1");

if (title) {
  title.textContent = "页面标题已更新";
}

querySelector 找到第一个匹配元素,找不到时返回 null。因此,在修改元素之前应确认它确实存在,尤其是脚本可能运行在多个页面、元素由条件渲染生成,或选择器容易写错时。

显示普通文本应优先使用 textContent。innerHTML 会把字符串当作 HTML 解析并创建元素;如果字符串包含用户可控内容,可能导致跨站脚本攻击(XSS)。例如下面的写法不安全:

// 不要把不可信输入直接拼入 innerHTML
container.innerHTML = userInput;

若要显示用户输入的原样文本,使用:

container.textContent = userInput;

需要插入富文本时,应使用经过维护、正确配置的 HTML 消毒方案,或通过 DOM API 逐个创建允许的元素和属性。单靠“过滤几个字符”通常不能可靠地净化 HTML。外部链接、URL 参数、表单内容和服务器返回值都应按照具体用途处理;编码或校验的方式取决于它们最终进入的位置。

八、事件:响应用户操作

通过 addEventListener 注册事件处理函数。点击按钮时执行回调,是制作交互页面最常见的起点:

const button = document.querySelector("#save-button");

if (button) {
  button.addEventListener("click", function () {
    console.log("按钮被点击");
  });
}

提交表单时通常监听表单的 submit 事件,而不只监听按钮点击。这样键盘提交也能被处理。若页面不打算通过传统方式跳转提交,可调用 event.preventDefault() 阻止默认行为:

const form = document.querySelector("#search-form");

form.addEventListener("submit", function (event) {
  event.preventDefault();

  const input = form.querySelector("input");
  console.log(input.value);
});

上面的示例假设表单和输入框确实存在;正式代码中应对选择结果进行检查。不要给同一个操作重复绑定事件,否则一次点击可能触发多次处理。动态生成许多相似元素时,可以考虑事件委托:把监听器放在共同的父元素上,再根据事件目标判断用户操作的是哪个子元素。

九、异步代码与网络请求

浏览器中的计时器、网络请求等操作通常不会立刻完成。JavaScript 可以先继续执行其他任务,等结果准备好后再处理。Promise 表示一个稍后完成或失败的操作;async 函数会返回 Promise,await 可用于等待 Promise 的结果。

async function loadData() {
  try {
    const response = await fetch("/api/tasks");

    if (!response.ok) {
      throw new Error("请求失败:" + response.status);
    }

    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error("无法加载数据:", error);
  }
}

fetch 遇到网络错误等情况会拒绝 Promise,但 HTTP 404 或 500 通常仍会得到一个 Response 对象,因此需要检查 response.ok 或状态码。调用 response.json() 也是异步操作,且响应内容不一定总是合法 JSON,因此应考虑捕获解析错误。

请求地址是否可访问,还受同源策略、服务器配置和认证方式等因素影响。浏览器控制台中的跨域错误通常需要由服务器正确配置 CORS 响应头,不能靠在前端代码里关闭浏览器安全限制解决。不要把服务器密钥、私密令牌或其他机密信息写进公开网页脚本:浏览器送到用户设备的代码可以被查看。

十、完成第一个交互页面:待办清单

下面的页面允许用户添加待办项、标记完成和删除项目。它把待办数据保存在页面内存中,刷新后内容会消失。示例只使用 textContent 写入用户输入,不把输入解析为 HTML。

把以下代码保存为 todo.html,然后用浏览器打开即可运行:

<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>待办清单</title>
  <style>
    body {
      max-width: 640px;
      margin: 40px auto;
      padding: 0 16px;
      font-family: sans-serif;
      line-height: 1.6;
    }

    form {
      display: flex;
      gap: 8px;
    }

    input {
      flex: 1;
      padding: 8px;
    }

    button {
      padding: 6px 10px;
      cursor: pointer;
    }

    li {
      display: flex;
      align-items: center;
      gap: 8px;
      margin: 8px 0;
    }

    li span {
      flex: 1;
    }

    li.done span {
      color: #666;
      text-decoration: line-through;
    }

    #message {
      min-height: 1.5em;
      color: #a22;
    }
  </style>
</head>
<body>
  <h1>待办清单</h1>

  <form id="todo-form">
    <label for="todo-input">新任务</label>
    <input id="todo-input" name="task" maxlength="120" required>
    <button type="submit">添加</button>
  </form>

  <p id="message" role="status" aria-live="polite"></p>
  <ul id="todo-list"></ul>

  <script>
    const form = document.querySelector("#todo-form");
    const input = document.querySelector("#todo-input");
    const list = document.querySelector("#todo-list");
    const message = document.querySelector("#message");

    const tasks = [];
    let nextId = 1;

    function renderTasks() {
      list.replaceChildren();

      for (const task of tasks) {
        const item = document.createElement("li");
        const text = document.createElement("span");
        const toggleButton = document.createElement("button");
        const deleteButton = document.createElement("button");

        item.classList.toggle("done", task.done);
        text.textContent = task.text;

        toggleButton.type = "button";
        toggleButton.textContent = task.done ? "恢复" : "完成";
        toggleButton.setAttribute(
          "aria-label",
          (task.done ? "恢复任务:" : "标记完成:") + task.text
        );

        toggleButton.addEventListener("click", function () {
          task.done = !task.done;
          renderTasks();
        });

        deleteButton.type = "button";
        deleteButton.textContent = "删除";
        deleteButton.setAttribute(
          "aria-label",
          "删除任务:" + task.text
        );

        deleteButton.addEventListener("click", function () {
          const index = tasks.findIndex(function (current) {
            return current.id === task.id;
          });

          if (index !== -1) {
            tasks.splice(index, 1);
            renderTasks();
          }
        });

        item.append(text, toggleButton, deleteButton);
        list.append(item);
      }
    }

    form.addEventListener("submit", function (event) {
      event.preventDefault();

      const text = input.value.trim();

      if (text.length === 0) {
        message.textContent = "请输入任务内容。";
        input.focus();
        return;
      }

      tasks.push({
        id: nextId,
        text: text,
        done: false
      });

      nextId += 1;
      input.value = "";
      message.textContent = "";
      renderTasks();
      input.focus();
    });
  </script>
</body>
</html>

代码做了什么

  1. 找到页面元素:通过 querySelector 保存表单、输入框、列表和提示区域的引用。由于脚本放在页面末尾,这些元素在执行脚本时已经创建。
  2. 维护数据:tasks 保存任务对象,nextId 为新任务生成当前页面内唯一的递增编号。编号只在当前页面会话中使用,不适合作为跨页面、跨用户的数据标识。
  3. 统一渲染:renderTasks 先清空列表,再根据数据创建新节点。数据变化后重新渲染,逻辑简单,适合这个小示例;大型列表可能需要增量更新,以减少不必要的 DOM 操作。
  4. 安全显示文本:任务文字通过 textContent 写入节点,即使用户输入包含类似 HTML 的字符,也会作为普通文本显示。
  5. 处理表单:代码阻止默认提交,去掉输入两端空白,并拒绝空内容。HTML 中的 required 和 maxlength="120" 提供浏览器端的基础约束,JavaScript 中的 trim() 进一步避免只输入空格。

maxlength 是浏览器对该输入框的客户端限制,不是可信的安全边界。它以浏览器规定的字符串长度单位限制用户输入,也不能保证请求经过其他客户端时遵守该限制。真实应用的服务端仍应重新验证字段长度、格式和业务规则;如果需要限制的是 UTF-8 字节数、数据库列容量或特定语言的字数,必须按对应规则检查,不能简单把 maxlength 当作等价限制。

同样,示例里的任务只存在于内存。若希望刷新后保留数据,可以研究 localStorage 或后端存储;但本地存储并不适合放置密码、访问令牌等敏感数据。保存在浏览器中的内容可能被同源脚本读取,也可能被用户清除或修改。需要多用户共享、权限控制或可靠备份时,应设计服务端存储和访问控制。

十一、常见错误与排查方法

1. 变量名称拼错

JavaScript 区分大小写,taskList 和 tasklist 是不同名称。遇到 ReferenceError,先检查名称拼写、变量是否已声明,以及代码是否在正确的作用域中运行。

2. 对不存在的元素操作

document.querySelector 可能返回 null。若随后调用 addEventListener,会报错。检查选择器是否正确、脚本执行时机是否合适,并在必要时判断元素是否存在。

3. 把字符串当作数字

输入框的 value 通常是字符串。若需要数值运算,应明确转换并验证结果。不要只用 parseInt 就假设输入完整有效:它可以解析字符串前段的整数,例如 parseInt("12px", 10) 得到 12。对必须是完整数字的输入,可使用严格格式校验后再转换,或校验转换结果和允许范围。

4. 忘记检查网络响应

请求返回 404 或 500 时,fetch 通常不会像网络断开那样自动进入 catch。读取响应前要检查 response.ok,并针对失败状态给用户适当反馈。

5. 用 innerHTML 拼接用户输入

这可能造成 XSS。普通文本用 textContent;确需渲染 HTML 时,使用合适的消毒策略和受控的内容来源。不要认为输入框限制、前端过滤或隐藏按钮能替代安全设计。

6. 只在前端做权限判断

前端可以改善使用体验,但不能决定用户是否有权读取或修改服务器数据。攻击者可以直接构造请求。服务端必须验证身份、权限、数据格式和业务规则。

7. 忽略错误信息

开发时打开浏览器控制台查看第一条错误,并定位到文件和行号。错误通常会沿调用链传递,先处理最早出现的问题。对于异步逻辑,要妥善处理 Promise 拒绝和异常,避免请求失败时页面静默失效。

十二、建议的练习路线

可以在待办页面基础上逐步添加功能,每次只改变一项并观察结果:

  • 增加“全部、未完成、已完成”筛选。
  • 显示待办总数和未完成数量。
  • 加入编辑功能,并限制标题长度。
  • 学习 localStorage,实现本机持久化,同时处理存储数据读取失败的情况。
  • 使用 fetch 从服务端读取数据,并分别处理加载中、成功和失败状态。
  • 把 HTML、CSS 和 JavaScript 拆分为独立文件,了解 ES 模块和浏览器网络面板。

学习 JavaScript 时,先掌握数据、条件、循环和函数,再学习 DOM、事件和异步代码。遇到新 API,查阅其适用环境、参数含义、失败方式和安全注意事项。把用户输入视为不可信数据,在前端做易用性校验,在服务端做权威校验,能让小练习逐步具备真实项目所需的可靠性。

© 版权声明

相关文章