你点开导航菜单,它立即展开;你修改购物车数量,总价随之更新;你提交表单,页面在发送前就指出漏填的内容。网页会对人的操作作出反应,通常都离不开 JavaScript。
不过,“会写一行 JavaScript”和“知道它为什么能运行”是两件事。同一段 JavaScript 代码既能在浏览器里运行,也能由 Node.js 执行;两种环境共享语言规则,却提供不同的能力。先把这条边界理清,遇到 document is not defined 之类的错误时就不会无从下手。
这篇教程会从一个最小实验开始,带你把代码放进网页、观察输出、接收一次输入,并完成由 HTML 文件和 JavaScript 文件组成的小项目。
完成本章后,你应该能够:
defer 的作用;alert()、prompt() 和 Math.random() 完成一次简单互动。
学习顺序不是术语清单,而是“运行—观察—解释—再组合”的循环。
这一章不要求你记住所有语法。先确保每个示例都亲手运行过,并且能判断它应该在浏览器还是 Node.js 中运行。
一个常见网页由三层协作完成:
把网页想成一家咖啡店:HTML 是桌椅和菜单,CSS 决定装修风格,JavaScript 则负责接单、计算价格并根据顾客选择作出反应。三者没有高低之分,职责不同而已。
JavaScript 也不只服务于网页。它还能编写服务端程序、命令行工具、自动化脚本和游戏逻辑。无论用在哪个方向,最基础的值、表达式、语句、函数和对象都遵守同一套语言规则。
下面是最小的 JavaScript 语句。它不会修改页面,只会向控制台发送一条消息:
console.log("页面脚本已经开始运行");运行后,控制台会显示:
页面脚本已经开始运行JavaScript 不是悬空运行的。浏览器标签页、Node.js 进程、服务器运行时等都会为代码提供一个“房间”,这个房间就是运行环境,也常被称为宿主环境。

语言核心规定代码怎么写,宿主环境决定代码能接触什么。
ECMAScript 定义 JavaScript 的核心语言规则。字符串、变量、typeof、Math.random() 和函数调用都属于语言层面的能力。浏览器与 Node.js 实现这些规则,同时再提供各自的 API。
console 在浏览器与 Node.js 中都很常见,但它仍是环境提供的调试接口,不是语言语法本身。fetch 也可能同时存在于现代浏览器和现代 Node.js 中;“两个环境都有”不等于“属于语言核心”。
globalThis 观察当前环境globalThis 提供访问当前环境全局对象的统一入口。下面的代码在 Node.js v25.2.1 中运行:
const message = "你好,JavaScript";
console.log(message);
console.log(typeof message);
console.log("document" in globalThis);
console.log("process" in globalThis);
console.log(typeof fetch);输出为:
你好,JavaScript
string
false
true
function前两行只使用语言核心。后面三行在探测环境能力:这个 Node.js 版本没有网页的 document,但有 process,也提供 fetch。换用其他版本或宿主时,应重新探测,不要凭印象假设 API 一定存在。
如果直接在 Node.js 里调用 document,代码会失败:
try {
document.querySelector("h1");
} catch (error) {
console.log(`${error.name}: ${error.message}`);
}ReferenceError: document is not defined这不是 querySelector() 的拼写规则导致的,而是当前环境根本没有定义 document。
看到错误时先做两次判断:代码是否符合 JavaScript 语法?它调用的名称是否由当前运行环境提供?这个顺序能快速缩小排查范围。
学习浏览器 JavaScript 时,最小组合只有两样:
编辑器最好具备语法高亮、自动补全、错误提示和格式化功能。它们不会替你理解代码,但能更早暴露漏写引号、括号不匹配等问题。无法安装软件时,也可以先使用能运行 HTML 与 JavaScript 的在线编辑器。
如果还想直接运行 .js 文件,可以另外安装 Node.js。假设当前目录有一个 app.js:
console.log("来自 app.js");在终端中进入该目录后运行:
node app.js你会看到:
来自 app.jsNode.js 适合运行脚本,但它不会自动把一个 .js 文件变成网页。需要页面能力时,仍要创建 HTML,并让浏览器加载它。
建议先建一个独立练习目录,避免路径混乱:
javascript-lab/
├── index.html
└── main.js浏览器开发者工具中的 Console(控制台)可以立即执行表达式并显示结果。通常可以通过页面右键菜单中的“检查”,或浏览器对应的开发者工具快捷键打开它;不同操作系统和浏览器的快捷键可能不同。

控制台把“猜代码会怎样”变成“运行后观察证据”。
直接在浏览器控制台输入表达式:
4 + 10按 Enter 后,控制台会回显:
14但写在 .js 文件里的表达式不会自动把结果展示给你。想主动输出,就调用 console.log():
console.log(4 + 10);
console.log("你好,控制台");14
你好,控制台常用的控制台方法不止一种:
console.log("普通信息");
console.error("需要关注的错误信息");
console.table([
{ name: "HTML", role: "结构" },
{ name: "JavaScript", role: "行为" },
]);console.error() 通常会用更醒目的样式显示信息;console.table() 会尝试以表格呈现数组或对象。具体颜色和排版由开发工具决定,不应把样式当成程序逻辑。
调试时可以遵循一个短循环:
浏览器只有在 HTML 通过 <script> 元素接入脚本后,才知道应该加载并执行哪段 JavaScript。小实验可以把代码直接写进 HTML;实际项目更常把代码放到独立文件。
下面是一份可以直接保存为 index.html 的完整页面:
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>内联脚本实验</title>
</head>
<body>
<h1>你好,JavaScript</
<!doctype html> 告诉浏览器按现代 HTML 规则解析页面。<head> 放元信息,<body> 放可见内容。这里把脚本放在正文末尾,浏览器解析到它时,前面的标题已经存在。
内联脚本适合一次性演示,但脚本变长后,HTML 结构和程序逻辑会混在一起,也不方便多个页面复用。
把两个文件放在同一目录:
javascript-lab/
├── index.html
└── main.js在 index.html 的 <head> 中接入脚本:
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>外部脚本实验</title>
<script src="./main.js" defer></script>
main.js 的内容如下:
console.log("来自独立的 main.js");刷新页面后,控制台应该显示:
来自独立的 main.js
src 负责找到文件,defer 负责安排合适的执行时机。
src 使用 URL 路径。先找到当前 HTML 文件,再从它所在的位置一步步走向脚本:
文件名和目录名要准确匹配。在区分大小写的服务器上,main.js 与 Main.js 是两个不同路径。不要把自己电脑上的完整磁盘路径写进网页;项目换目录或部署后,这类路径通常会失效。
defer、async 与模块脚本普通外部脚本可能在下载和执行期间阻塞 HTML 解析。初学阶段,把普通外部脚本写在 <head> 时可以加 defer:
<script src="./main.js" defer></script>defer 会让浏览器继续解析 HTML,等文档解析完成后再按页面中的先后顺序执行这些脚本。async 则会在文件下载完成后尽快执行,多个脚本之间不保证原有顺序,因此更适合彼此独立的脚本。
模块脚本使用 type="module",默认具有延后执行的行为:
<script type="module" src="./main.js"></script>这一章只需掌握普通外部脚本加 defer 的写法;模块的导入与导出可以在理解函数和作用域后再系统学习。
机器通常不在乎你多写了几个空格,但未来的你和团队成员会在乎。可读性不是最后再补的装饰,而是降低错误率的工具。
const price = 30;
const count = 2;
const total = price * count;
console.log(`总价:${total}`);总价:60price、count 和 total 分别表达单价、数量与总价。相比把所有内容挤进一行,这种写法更容易检查每一步。
下面的条件语句现在不要求你完全掌握,先观察花括号与缩进的关系:
const isReady = true;
if (isReady) {
console.log("开始执行");
} else {
console.log("继续准备");
}花括号里的代码统一缩进一级。缩进本身通常不改变 JavaScript 的含义,但能让人一眼看出哪些语句属于同一个代码块。项目应统一使用自动格式化工具或一致的空格规则。
JavaScript 存在自动分号插入机制,所以许多换行处即使不写分号也能运行,但少数写法会产生意外解析结果。初学示例统一在独立语句末尾写分号:
const course = "JavaScript";
console.log(course);if (...) { ... }、函数体和循环体末尾的右花括号通常不因为“结束了一个代码块”就额外添加分号。真正进入团队项目后,应跟随项目已有的格式化与检查规则。
单行注释从 // 开始,多行注释写在 /* 与 */ 之间:
// 使用含税价格,避免结算页再次重复计算
const finalPrice = 68;
/*
下面先保留一条调试日志。
确认数据正确后再移除。
*/
console.log(finalPrice);注释不会被执行。它适合说明背景、限制和不明显的设计原因,也可以在调试时临时停用一行代码:
// console.log("这条日志暂时不执行");不要用注释逐字复述清楚的代码,也不要长期保留大段已经失效的“僵尸代码”;版本管理工具更适合保存历史。
控制台输出是单向反馈。为了快速体验“程序接收输入再产生结果”,浏览器页面还提供 alert() 和 prompt():
alert("欢迎来到 JavaScript 实验室");
const nickname = prompt("请输入你的昵称");
console.log("用户输入:", nickname);alert() 显示一个提示框。prompt() 显示输入框,并把用户提交的文本返回给代码;如果用户选择取消,结果是 null。它们属于浏览器页面 API,在普通 Node.js 脚本中不能直接使用。
这些模态对话框会暂停页面交互,适合学习和快速验证,不适合复杂的正式产品界面。真实项目通常会使用页面中的表单、按钮和自定义提示组件。
Math.random() 每次返回一个大于等于 0、小于 1 的随机小数。把它乘以 6 后范围变为 0 到小于 6,再向下取整并加 1,就得到 1 到 6 的整数:
const dice = Math.floor(Math.random() * 6) + 1;
console.log(`骰子点数:${dice}`);输出会随运行而变化,例如可能是 骰子点数:4。不能把某一次结果当成固定答案。
可以把输入与随机数放在一起:
const nickname = prompt("请输入你的昵称");
const luckyNumber = Math.floor(Math.random() * 100) + 1;
console.log("昵称:", nickname);
console.log("幸运数字:", luckyNumber);这里的随机整数范围是 1 到 100。即使暂时还没系统学习运算符,也可以先记住结构:向下取整(随机小数 × 数量) + 起始值。
现在创建一个“访客幸运数字”页面。项目目标不是做复杂界面,而是完整走通“创建文件—连接脚本—接收输入—观察输出—调试修改”的流程。
visitor-lab/
├── index.html
└── main.js将下面内容保存到 index.html:
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>访客幸运数字</title>
<script src="./main.js" defer></script>
将下面内容保存到 main.js:
/*
收集一次访客输入,并生成 1 到 100 的随机整数。
本练习只在浏览器页面中运行。
*/
alert("欢迎来到访客实验室");
const nickname = prompt("请输入你的昵称");
const luckyNumber = Math.floor(Math.random() * 100) + 1;
console.log("用户输入:", nickname);
console.log("幸运数字:", luckyNumber);index.html;main.js 中给第二条 console.log() 前加 //,刷新并确认该行不再输出;main.js,HTML 通过 ./main.js 正确连接;1 到 100 的整数;如果页面出现但控制台没有日志,依次检查:开发者工具是否打开、页面是否刷新、src 路径和文件名大小写是否一致、控制台是否出现红色错误。
document 属于浏览器,process 属于 Node.js。console.log() 主动展示观察结果。src 接入可复用的外部 .js 文件。<head> 时可配合 defer。alert() 与 prompt() 适合浏览器入门实验;取消 prompt() 时会得到 null。Math.floor(Math.random() * n) + 1 可以生成 1 到 n 的随机整数,单次输出不是固定结果。