在 HTML 中引入 JavaScript 有哪几种方式?它们各自的优缺点是什么?
核心答案
在 HTML 中引入 JavaScript 有 3 种主要方式:
| 方式 |
语法 |
主要场景 |
| 1. 行内式 |
<div onclick="alert('hi')"> |
极少使用,不推荐 |
| 2. 内嵌式 |
<script>alert('hi')</script> |
小型脚本、单页应用 |
| 3. 外链式 |
<script src="app.js"></script> |
生产环境首选 |
核心原则:生产环境优先使用外链式,配合 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 在同一文件,便于调试
- ✓ 可以访问页面中的所有元素
缺点:
- ❌ HTML 文件体积变大
- ❌ 无法被浏览器缓存(每次加载 HTML 都要重新加载 JS)
- ❌ 多个页面无法共享同一份 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 文件,提升加载速度
- ✅ 便于维护:代码独立管理
- ✅ 支持模块化:方便团队协作
缺点:
- ⚠️ 需要额外的 HTTP 请求(但可通过缓存和打包优化)
2. <script> 标签的关键属性
defer 和 async 的区别
页面解析流程对比:
无属性(默认):
HTML解析 → 遇到script → 停止解析 → 下载JS → 执行JS → 继续解析HTML
↑ 阻塞页面渲染 ↑
defer:
HTML解析 → 并行下载JS → HTML解析完成 → 按顺序执行JS → DOMContentLoaded
↓ 不阻塞解析 ↓
async:
HTML解析 → 并行下载JS → 下载完立即执行 → 继续解析HTML
↓ 执行时机不确定 ↓
| 属性 |
执行时机 |
顺序保证 |
适用场景 |
| 无属性 |
立即执行,阻塞解析 |
✅ 按顺序 |
无 |
| defer |
HTML 解析完成后,DOMContentLoaded 前 |
✅ 按顺序 |
DOM 操作脚本 |
| async |
下载完成后立即执行 |
❌ 无顺序保证 |
独立脚本(如统计、广告) |
<!-- 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>
<!-- 不保证执行顺序,谁先下载完谁先执行 -->
其他重要属性
| 属性 |
作用 |
示例 |
type |
指定脚本类型 |
type="module"(ES 模块) |
crossorigin |
CORS 配置 |
crossorigin="anonymous" |
integrity |
SRI(子资源完整性校验) |
integrity="sha384-..." |
nomodule |
不支持模块的浏览器才执行 |
<script nomodule src="legacy.js"></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'));
}
面试技巧
面试官可能的追问
-
"为什么传统建议把 <script> 放在 </body> 之前?"
-
"现在有了 defer,还需要放 </body> 前吗?"
- 不需要,
defer 可放 head,效果相同且更早开始下载
-
"什么情况下用 async?"
-
"多个 defer 脚本的执行顺序?"
-
"defer 和 DOMContentLoaded 的关系?"
-
defer 脚本在 DOMContentLoaded 之前执行
-
"什么是脚本阻塞(render blocking)?"
- 解释浏览器解析 HTML 时遇到
<script> 停止渲染的机制
如何展示深度理解
-
谈性能优化:
- 关键渲染路径优化
- 资源预加载(preload/prefetch)
- 代码分割(code splitting)
-
谈实际项目经验:
- 如何处理第三方脚本(如 Google Analytics)
- 如何优化首屏加载时间
- 使用过哪些构建工具的优化
-
谈浏览器兼容性:
-
defer/async 的浏览器支持情况
- 如何为老浏览器做降级处理
-
谈安全:
- SRI(Subresource Integrity)
- nonce/CSP(Content Security Policy)
一句话总结
外链式 + defer 是现代网页引入 JavaScript 的最佳实践,它实现了代码分离、可缓存、不阻塞渲染的完美平衡。