在 HTML 中引入 JavaScript 有哪几种方式?它们各自的优缺点是什么?
核心答案
在 HTML 中引入 JavaScript 有 3 种主要方式:

核心原则:生产环境优先使用外链式,配合 defer 或 async 优化加载性能。
深入解析
1. 三种方式详解
方式一:行内式(Inline)
<!-- 直接在 HTML 属性中写 JS -->
<button onclick="alert('点击了!')">点击我</button>
<a href="javascript:void(0)" onmouseover="console.log('悬停')">链接</a>
优点:
缺点:
❌ HTML 和 JS 强耦合,难以维护
❌ 无法复用逻辑
❌ 代码混乱,可读性差
❌ 存在 XSS 安全风险
❌ 无法利用浏览器缓存
方式二:内嵌式(Internal / Embedded)
<!DOCTYPE html>
<html>
<head>
<script>
// JS 代码写在 <script> 标签内
function init() {
console.log('页面初始化');
}
</script>
</head>
<body>
<h1>内嵌式示例</h1>
</body>
</html>
优点:
✓ 适合单页应用或小型项目
✓ HTML 和 JS 在同一文件,便于调试
✓ 可以访问页面中的所有元素
缺点:
方式三:外链式(External)⭐ 推荐
<!-- 基础用法 -->
<script src="js/app.js"></script>
<!-- 推荐用法:配合 defer -->
<script src="js/app.js" defer></script>
<!-- 或者 async(取决于场景) -->
<script src="js/analytics.js" async></script>
优点:
✅ HTML 与 JS 分离,结构清晰
✅ 可复用:多个页面共享同一个 JS 文件
✅ 可缓存:浏览器缓存 JS 文件,提升加载速度
✅ 便于维护:代码独立管理
✅ 支持模块化:方便团队协作
缺点:
2. <script> 标签的关键属性
defer 和 async 的区别
页面解析流程对比:
无属性(默认):
HTML解析 → 遇到script → 停止解析 → 下载JS → 执行JS → 继续解析HTML
↑ 阻塞页面渲染 ↑
defer:
HTML解析 → 并行下载JS → HTML解析完成 → 按顺序执行JS → DOMContentLoaded
↓ 不阻塞解析 ↓
async:
HTML解析 → 并行下载JS → 下载完立即执行 → 继续解析HTML
↓ 执行时机不确定 ↓
<!-- defer 推荐用法 -->
<script src="main.js" defer></script>
<script src="utils.js" defer></script>
<!-- 保证:utils.js 一定在 main.js 之前执行 -->
<!-- async 用法 -->
<script src="analytics.js" async></script>
<script src="ads.js" async></script>
<!-- 不保证执行顺序,谁先下载完谁先执行 -->
其他重要属性
3. 底层机制:浏览器如何加载和执行脚本
┌─────────────────────────────────────────────────────────┐
│ 浏览器渲染流程 │
├─────────────────────────────────────────────────────────┤
│ │
│ 1. HTML Parser ──→ 构建 DOM 树 │
│ ↓ │
│ 2. CSS Parser ──→ 构建 CSSOM 树 │
│ ↓ │
│ 3. 合并 ──→ 渲染树(Render Tree) │
│ ↓ │
│ 4. Layout(布局) │
│ ↓ │
│ 5. Paint(绘制) │
│ │
└─────────────────────────────────────────────────────────┘
遇到 <script> 时:
默认行为:
┌─────────┐
│ 停止解析 │ ← 阻塞 DOM 构建
└────┬────┘
↓
┌─────────┐
│ 下载 JS │ ← 如果是外链脚本
└────┬────┘
↓
┌─────────┐
│ 执行 JS │ ← 阻塞渲染
└────┬────┘
↓
┌─────────┐
│ 继续解析 │
└─────────┘
使用 defer/async:
┌─────────┐ ┌─────────┐
│继续解析 │ ←→ │并行下载 │ ← 不阻塞
└─────────┘ └─────────┘
4. 最佳实践
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>最佳实践示例</title>
<!-- CSS 放在 head 中 -->
<link rel="stylesheet" href="styles.css">
<!-- 预加载关键脚本 -->
<link rel="preload" href="critical.js" as="script">
</head>
<body>
<!-- 页面内容 -->
<!-- 方案1:现代浏览器推荐 -->
<script src="main.js" defer></script>
<script src="app.js" defer></script>
<!-- 方案2:需要立即执行的脚本(如 polyfill) -->
<script>
// 同步执行的小型脚本
</script>
<!-- 方案3:独立第三方脚本 -->
<script src="analytics.js" async></script>
<!-- 方案4:ES 模块 -->
<script type="module" src="module.js"></script>
<!-- 方案5:模块降级方案 -->
<script type="module" src="modern.js"></script>
<script nomodule src="legacy.js"></script>
</body>
</html>
5. 常见误区
❌ 误区1:defer 和 async 功能一样
✅ 纠正:defer 保证顺序且在 DOMContentLoaded 前执行,async 不保证顺序
❌ 误区2:把所有 <script> 都放在 <head> 里
✅ 纠正:传统放 </body> 前,现代用 defer 可放 head
❌ 误区3:defer 的脚本一定在 DOMContentLoaded 前执行
✅ 纠正:大部分情况是的,但如果脚本很大或网络慢,可能在之后
❌ 误区4:多个 async 脚本按书写顺序执行
✅ 纠正:async 脚本按下载完成顺序执行,顺序不可控
代码示例
示例1:三种引入方式对比
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>JS 引入方式对比</title>
<!-- 方式1:行内式(不推荐) -->
<button onclick="handleClick()">行内式按钮</button>
<!-- 方式2:内嵌式 -->
<script>
function handleClick() {
console.log('内嵌式函数被调用');
}
// 内嵌式可以直接操作页面
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM 加载完成');
});
</script>
<!-- 方式3:外链式(推荐) -->
<script src="js/utils.js" defer></script>
</head>
<body>
<h1>三种引入方式</h1>
<!-- 行内式的完整示例 -->
<div onmouseover="this.style.background='yellow'"
onmouseout="this.style.background='white'">
鼠标悬停变色
</div>
</body>
</html>
示例2:defer vs async 实际效果
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>defer vs async</title>
</head>
<body>
<h1>页面标题</h1>
<p>内容...</p>
<script>
// 同步脚本:阻塞后续渲染
console.log('1. 同步脚本开始');
// 模拟耗时操作
const start = Date.now();
while (Date.now() - start < 2000) {}
console.log('2. 同步脚本结束(阻塞了2秒)');
</script>
<p>这行内容被延迟显示了</p>
<!-- defer 脚本 -->
<script src="defer1.js" defer></script>
<script src="defer2.js" defer></script>
<!-- 保证:defer1.js 在 defer2.js 之前执行 -->
<!-- async 脚本 -->
<script src="async1.js" async></script>
<script src="async2.js" async></script>
<!-- 不保证:谁先下载完谁先执行 -->
<script>
document.addEventListener('DOMContentLoaded', function() {
console.log('3. DOMContentLoaded 触发');
});
window.addEventListener('load', function() {
console.log('4. 页面完全加载完成');
});
</script>
</body>
</html>
示例3:现代项目的标准引入方式
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>现代项目</title>
<!-- 预连接到 CDN -->
<link rel="preconnect" href="https://cdn.example.com">
<!-- 预加载关键资源 -->
<link rel="preload" href="critical.css" as="style">
<link rel="preload" href="critical.js" as="script">
<!-- 关键 CSS -->
<link rel="stylesheet" href="critical.css">
<!-- Polyfill:需要立即执行且不依赖 DOM -->
<script>
// 检测和添加必要的 polyfill
if (!window.Promise) {
document.write('<script src="polyfills/promise.js"><\/script>');
}
</script>
</head>
<body>
<div id="app"></div>
<!-- 主要应用脚本:使用 defer -->
<script src="vendors.js" defer></script>
<script src="main.js" defer></script>
<!-- 第三方统计:使用 async -->
<script src="analytics.js" async></script>
<!-- ES 模块 + 降级方案 -->
<script type="module" src="modern-app.js"></script>
<script nomodule src="legacy-app.js"></script>
</body>
</html>
示例4:动态加载脚本
// 动态创建 script 标签
function loadScript(url, options = {}) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = url;
// 设置属性
if (options.async) script.async = true;
if (options.defer) script.defer = true;
if (options.type) script.type = options.type;
// 事件监听
script.onload = () => resolve(script);
script.onerror = () => reject(new Error(`Failed to load ${url}`));
document.head.appendChild(script);
});
}
// 使用示例
async function initApp() {
try {
await loadScript('/utils.js', { defer: true });
await loadScript('/main.js', { defer: true });
console.log('所有脚本加载完成');
} catch (error) {
console.error('脚本加载失败:', error);
}
}
// 条件加载
if ('IntersectionObserver' in window) {
// 支持,加载现代版本
loadScript('/modern-image-lazy-load.js');
} else {
// 不支持,加载 polyfill
loadScript('/polyfills/intersection-observer.js')
.then(() => loadScript('/legacy-image-lazy-load.js'));
}
一句话总结
外链式 + defer 是现代网页引入 JavaScript 的最佳实践,它实现了代码分离、可缓存、不阻塞渲染的完美平衡。
参考文章:原文链接
该文章在 2026/2/3 17:24:12 编辑过