JavaScript <script> 标签
在搭建好开发环境并编写了你的第一个 JavaScript 程序 "Hello, World!" 之后,你可能会好奇:这段 JavaScript 代码究竟是如何被网页链接并执行的呢?
这就是 <script> 标签发挥作用的地方。
<script> 标签是一个基础的 HTML 元素,它充当了 HTML 结构与动态 JavaScript 功能之间的桥梁。它告诉 Web 浏览器:“嘿,这里有一些可执行的代码,请处理它!”
对于任何 Web 开发者来说,理解如何以及在哪里使用这个标签至关重要,因为它的位置和属性会显著影响网站的性能、用户体验,甚至决定你的 JavaScript 代码能否正常工作。
1. 嵌入 JavaScript:内联脚本与外链脚本
使用 <script> 标签将 JavaScript 包含在 HTML 文档中有两种主要方式:作为内联脚本 (Inline Script) 或外链脚本 (External Script)。每种方法都有其特定的用例和含义。
1.1 内联 JavaScript (Inline JavaScript)
内联 JavaScript 意味着直接在 HTML 文件内部的 <script> 标签之间编写 JavaScript 代码。这种方法对于小型脚本或快速测试非常直观,但出于可维护性和性能的考虑,通常不建议用于大型项目。
语法:
<script>
// 你的 JavaScript 代码写在这里
</script>1.1.1 示例:在 <head> 中的内联脚本
当 <script> 标签被放置在 HTML 文档的 <head> 部分时,浏览器会在解析页面的 <body> 之前遇到并执行这段 JavaScript 代码。这意味着,如果你的脚本试图与尚未加载的 HTML 元素进行交互,它很可能会失败。
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Head 中的内联脚本</title>
<script>
// 这个脚本一旦被浏览器遇到就会运行。
// 如果它试图修改一个尚未加载的元素,它将无法工作。
// document.getElementById('myParagraph').textContent = 'Head 脚本修改了内容!'; // 这行代码会失败!
console.log("来自 <head> 脚本的问候!"); // 我们仍然可以向控制台输出日志。
</script>
</head>
<body>
<p id="myParagraph">原始段落内容。</p>
</body>
</html>在这个例子中,console.log 消息会在段落元素完全在页面上渲染之前出现在浏览器的开发者控制台中。如果我们取消注释那行试图访问 myParagraph 的代码,它会抛出一个错误,因为在脚本执行时,myParagraph 尚不存在于文档对象模型 (DOM) 中。
1.1.2 示例:在 <body> 中的内联脚本
将 <script> 标签放置在 <body> 部分的末尾(就在关闭标签 </body> 之前)是一种常见的做法。这确保了脚本上方的 HTML 内容已经被解析和渲染,使得你的 JavaScript 可以安全地与这些元素进行交互。
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Body 中的内联脚本</title>
</head>
<body>
<p id="myParagraph">原始段落内容。</p>
<script>
// 这个脚本在 'myParagraph' 元素被解析后运行。
const paragraphElement = document.getElementById('myParagraph');
if (paragraphElement) {
paragraphElement.textContent = 'Body 脚本修改了内容!';
}
console.log("来自 <body> 脚本的问候!");
</script>
</body>
</html>在这里,脚本在 myParagraph 元素存在后运行,因此它可以成功找到并修改其内容。这演示了一个核心概念:在混合使用 HTML 和 JavaScript 时,执行顺序非常重要。
1.2 外链 JavaScript 文件 (External Javascript Files)
对于任何有一定规模的 JavaScript 代码,最佳实践是将代码编写在单独的 .js 文件中,然后使用 <script> 标签的 src 属性将其链接到 HTML 文档。
语法:
<script src="path/to/your-script.js"></script>外链 JavaScript 的好处:
- 关注点分离 (Separation of Concerns): 保持 HTML 干净且专注于结构,而 JavaScript 专注于行为。这是优秀 Web 开发的一个基本原则。
- 可缓存性 (Cacheability): 浏览器可以缓存外部
.js文件。如果用户再次访问你的网站,浏览器不需要重新下载 JavaScript,从而加快页面加载速度。 - 可复用性 (Reusability): 同一个 JavaScript 文件可以链接到多个 HTML 页面,避免代码重复。
- 易于维护 (Easier Maintenance): 修改 JavaScript 逻辑只需在一个
.js文件中进行,而无需在多个 HTML 文件中查找。
1.2.1 示例:链接一个外链脚本
首先,在与你的 index.html 文件相同的目录中(或者在像 js/script.js 这样的子文件夹中)创建一个名为 script.js(或其他有意义的名字)的文件。
script.js:
// 这个脚本将在被 HTML 文档加载时运行。
console.log("来自外链脚本的问候!");
// 让我们尝试修改一个元素
const headerElement = document.getElementById('mainHeader');
if (headerElement) {
headerElement.textContent = '欢迎来到我们的 Javascript 页面!';
}index.html
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>外链脚本示例</title>
</head>
<body>
<h1 id="mainHeader">加载中...</h1>
<p>这是一些内容。</p>
<script src="script.js"></script>
</body>
</html>当 index.html 文件加载时,浏览器会先解析 HTML,然后获取并执行 script.js。因为 script.js 放在 <body> 的末尾,所以当脚本运行时,带有 id="mainHeader" 的 h1 元素已经存在于 DOM 中,允许 JavaScript 成功更新其内容。
2. 使用 defer 和 async 属性控制脚本加载
现代 Web 开发通常涉及许多 JavaScript 文件,这可能会拖慢页面渲染速度。默认情况下,当浏览器遇到 <script> 标签时,它会暂停 HTML 解析,获取脚本(如果是外链),执行它,然后才恢复 HTML 解析。这会给用户带来明显的延迟,尤其是对于大脚本或慢速网络连接。
defer 和 async 属性有助于优化这种行为。
2.1 defer 属性
当 defer 属性被添加到外链 <script> 标签时,它告诉浏览器:“并行下载此脚本与 HTML 解析,但仅在 HTML 文档完全解析后才执行它。”
defer 的关键特征:
- 非阻塞下载: 浏览器在下载脚本的同时继续解析 HTML。
- 延迟执行: 脚本仅在 HTML 文档完全解析且 DOM 准备就绪后执行。
- 保留顺序: 如果你有多个 defer 脚本,它们将按照在 HTML 中出现的顺序执行。
- 最适合: 依赖于完整 HTML 文档(例如,交互元素、表单验证)且执行顺序很重要的脚本。
语法:
<script src="path/to/your-script.js" defer></script>示例: 想象你有两个脚本,script1.js 和 script2.js,其中 script2.js 依赖于 script1.js 中定义的内容。
script1.js:
console.log("脚本 1 已加载。");
window.myVariable = "来自脚本 1 的问候!";script2.js:
console.log("脚本 2 已加载。");
if (window.myVariable) {
console.log("脚本 2 访问到了: " + window.myVariable);
} else {
console.log("脚本 2: myVariable 尚未找到!");
}index.html:
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Defer 示例</title>
</head>
<body>
<h1>Defer 属性演示</h1>
<p>观察控制台中的脚本加载顺序。</p>
<script src="script1.js" defer></script>
<script src="script2.js" defer></script>
</body>
</html>当 index.html 加载时,script1.js 和 script2.js 都会在 HTML 解析的同时在后台下载。HTML 完全解析后,script1.js 将执行,然后 script2.js 执行。这确保了 script2.js 能看到由 script1.js 定义的 myVariable。
2.2 async 属性
async 属性同样用于外链 <script> 标签,它告诉浏览器:“并行下载此脚本与 HTML 解析,并且一旦下载完成就立即执行,无需等待 HTML 解析完成或其他脚本。”
async 的关键特征:
- 非阻塞下载: 浏览器在下载脚本的同时继续解析 HTML。
- 独立执行: 脚本一旦准备好就立即执行,即使 HTML 解析尚未完成。
- 无保证顺序: 如果你有多个
async脚本,它们将以任意顺序执行(谁先下载完谁先执行)。这意味着script1.js可能会在script2.js之后运行,如果script2.js下载得更快。 - 最适合: 不依赖于其他脚本的顺序或完整 DOM 的独立脚本(例如,分析脚本、广告)。
语法:
<script src="path/to/your-script.js" async></script>示例: 使用与之前相同的 script1.js 和 script2.js,但这次使用 async。
index.html:
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Async 示例</title>
</head>
<body>
<h1>Async 属性演示</h1>
<p>仔细观察控制台。执行顺序可能会有所不同。</p>
<script src="script1.js" async></script>
<script src="script2.js" async></script>
</body>
</html>在这种情况下,script1.js 和 script2.js 并行下载。无论哪个脚本先下载完成,都会立即执行。这里无法保证 script1.js 会在 script2.js 之前执行。这意味着 script2.js 可能会在 script1.js 有机会定义 myVariable之前执行,从而导致 脚本 2: myVariable 尚未找到! 的消息。
2.3 type="module" (简介)
虽然深入探讨 JavaScript 模块超出了本入门课程的范围,但值得简要提及 type="module" 属性。此属性用于导入和导出模块,模块本质上是独立的 JavaScript 文件,它们封装了特定功能并可以以受控方式相互共享数据。当你使用 type="module" 时,脚本默认会被延迟执行(类似于 defer 的行为)并在严格模式下运行。
语法:
<script src="path/to/module.js" type="module"></script>你将在未来的课程中了解更多关于模块的信息,但现在,只需明白这是一种构建大型 JavaScript 应用程序的现代方式。
3. 实践示例与演示
让我们通过一个综合示例将所有内容结合起来。
文件结构:
my-project/
├── index.html
└── js/
├── head-script.js
├── body-script.js
├── deferred-script.js
└── async-script.jsjs/head-script.js:
// 此脚本在 <head> 中链接,没有使用 defer/async
console.log("1. head-script.js: 在 HTML 仍在解析时立即执行。");
// 试图修改一个可能尚不存在的元素
// document.getElementById('greeting').textContent = '由 head 脚本修改(如果无 defer 可能会失败)!';js/body-script.js:
// 此脚本链接在 <body> 的末尾,没有使用 defer/async
console.log("3. body-script.js: 在 HTML 解析到此处后执行。");
// 这应该能成功修改 greeting 元素
document.getElementById('greeting').textContent = '由 body 脚本修改!';js/deferred-script.js:
// 此脚本使用 'defer' 链接
console.log("4. deferred-script.js: 并行下载,在 HTML 解析完成后执行。");
document.getElementById('info').innerHTML += '<p>Deferred 脚本添加了这段话。</p>';js/async-script.js:
// 此脚本使用 'async' 链接
console.log("2. async-script.js: 并行下载,一旦准备好就立即执行(顺序不保证)。");
// 注意:根据网络速度,这条日志可能出现在 3 或 4 之前
document.getElementById('async-message').textContent = 'Async 脚本更新了这里!';index.html:
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>深入 Script 标签</title>
<script src="js/head-script.js"></script>
<script src="js/async-script.js" async></script>
</head>
<body>
<h1 id="greeting">你好, Javascript!</h1>
<p id="info">这是一些初始内容。</p>
<p id="async-message">原始 async 消息。</p>
<script>
console.log("Body: 内联脚本已执行。");
document.getElementById('info').innerHTML += '<p>内联脚本添加了这段话。</p>';
</script>
<script src="js/deferred-script.js" defer></script>
<script src="js/body-script.js"></script>
</body>
</html>当你在浏览器中打开 index.html 并检查开发者控制台(你在“你的第一个 JavaScript 程序:'Hello, World!'”中学到的)时,你会观察到日志消息。
"2. async-script.js" 的确切顺序可能会有所不同,但 "1. head-script.js" 通常会首先出现,紧接着是内联 body 脚本,然后是 "4. deferred-script.js",最后是 "3. body-script.js"。页面内容(h1, p)也会反映出脚本所做的更改。