
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>
代码做了什么
- 找到页面元素:通过
querySelector保存表单、输入框、列表和提示区域的引用。由于脚本放在页面末尾,这些元素在执行脚本时已经创建。 - 维护数据:
tasks保存任务对象,nextId为新任务生成当前页面内唯一的递增编号。编号只在当前页面会话中使用,不适合作为跨页面、跨用户的数据标识。 - 统一渲染:
renderTasks先清空列表,再根据数据创建新节点。数据变化后重新渲染,逻辑简单,适合这个小示例;大型列表可能需要增量更新,以减少不必要的 DOM 操作。 - 安全显示文本:任务文字通过
textContent写入节点,即使用户输入包含类似 HTML 的字符,也会作为普通文本显示。 - 处理表单:代码阻止默认提交,去掉输入两端空白,并拒绝空内容。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,查阅其适用环境、参数含义、失败方式和安全注意事项。把用户输入视为不可信数据,在前端做易用性校验,在服务端做权威校验,能让小练习逐步具备真实项目所需的可靠性。









