JavaScript 是一种轻量级的脚本语言,脚本语言指的是它不具备开发操作系统的能力,而是只用来编写控制其他大型应用程序(比如浏览器)的脚本
⚠️
JavaScript也是一种嵌入式语言,本身提供的核心语法不算多,只能用来做一些数学和逻辑运算。并且本身不提供任何与I/O(输入/输出)相关的API,都要靠宿主环境提供,所以JavaScript只合适嵌入更大型的应用程序环境,去调用宿主环境提供的底层 API 目前已经嵌入JavaScript的宿主环境有多种,最常见的环境就是浏览器,另外还有服务器环境,也就是Node项目 从语法角度看,JavaScript语言是一种“对象模型”语言。各种宿主环境通过这个模型,描述自己的功能和操作接口,从而通过JavaScript控制这些功能。但是JavaScript并不是纯粹的“面向对象语言”,还支持其他编程范式(比如函数式编程)。这导致几乎任何一个问题,JavaScript都有多种解决方法
JavaScript 是门用来与网页交互的脚本语言,包含以下三个组成部分:
ECMAScript:由 ECMA-262 定义并提供核心功能JavaScript 核心语法部分相当精简,只包括两个部分:
Array、Date、Math 等)除此之外,各种宿主环境提供额外的 API 供 JavaScript 调用。以浏览器为例,它提供的额外 API 可以分成三大类:
如果宿主环境是服务器,则会提供各种操作系统的 API,比如文件操作 API、网络通信 API 等等
本文主要介绍 JavaScript 核心语法和浏览器网页开发的基本知识,不涉及 Node
JavaScript 发明目的,就是作为浏览器的内置脚本语言,为网页开发者提供操控浏览器的能力。它是目前唯一一种通用的浏览器脚本语言,所有浏览器都支持。它可以让网页呈现各种特殊效果,为用户提供良好的互动体验。
目前全世界几乎所有网页都使用 JavaScript。如果不用,网站的易用性和使用效率将大打折扣,无法成为操作便利、对用户友好的网站
随着 HTML5 的出现,浏览器本身的功能越来越强,不再仅仅能浏览网页,而是越来越像一个平台,JavaScript 因此得以调用许多系统功能,比如操作本地文件、操作图片、调用摄像头和麦克风等等。这使得 JavaScript 可以完成许多以前无法想象的事情
Node 项目使得 JavaScript 可以用于开发服务器端的大型项目,网站的前后端都用 JavaScript 开发已经成为了现实。有些嵌入式平台(Raspberry Pi)能够安装 Node,于是 JavaScript 就能为这些平台开发应用程序
Deno 是由 Node.js 原作者 Ryan Dahl 于 2018 年创建的新 JavaScript/TypeScript 运行时,旨在解决 Node.js 的设计缺陷:
.ts 文件fetch、WebSocket、URL 等浏览器标准 APIimport/export 而非 require()Bun 是 2022 年推出的高性能 JavaScript/TypeScript 运行时,使用 JavaScriptCore 引擎:
Bun.file()、Bun.serve()、Bun.password 等高效内置方法JavaScript 甚至也可以用来操作数据库。NoSQL 数据库这个概念,本身就是在 JSON 格式的基础上诞生的,大部分 NoSQL 数据库允许 JavaScript 直接操作。基于 SQL 语言的开源数据库 PostgreSQL 支持 JavaScript 作为操作语言,可以部分取代 SQL 查询语言
JavaScript 也正在成为手机应用的开发语言。一般来说,安卓平台使用 Java 语言开发,iOS 平台使用 Objective-C 或 Swift 语言开发。许多人正在努力,让 JavaScript 成为各个平台的通用开发语言
PhoneGap 项目就是将 JavaScript 和 HTML5 打包在一个容器之中,使得它能同时在 iOS 和安卓上运行。Facebook 公司的 React Native 项目则是将 JavaScript 写的组件,编译成原生组件,从而使它们具备优秀的性能。此外,Expo 基于 React Native 提供了更简化的开发体验,Capacitor(Ionic 团队)则是 PhoneGap 的现代替代方案
越来越多的应用程序,将 JavaScript 作为内嵌的脚本语言,比如 Adobe 公司的著名 PDF 阅读器 Acrobat、Linux 桌面环境 GNOME 3
Chromium OS、Windows 8 等操作系统直接支持 JavaScript 编写应用程序。GitHub 的 Electron 项目 可以用来编写运行于 Windows、macOS 和 Linux 等多个桌面平台的程序。近年来,Tauri 作为 Electron 的轻量级替代方案,使用系统原生 WebView 替代 Chromium,显著减小了打包体积。
著名程序员 Jeff Atwood 甚至提出了一条 “Atwood 定律”:
所有可以用 JavaScript 编写的程序,最终都会出现 JavaScript 的版本。(Any application that can be written in JavaScript will eventually be written in JavaScript)
JavaScript 的性能优势体现在以下方面:
JavaScript 既支持类似 C 语言清晰的过程式编程,也支持灵活的函数式编程,可以用来写并发处理(concurrent)。这些语法特性已经被证明非常强大,可以用于许多场合,尤其适用异步编程。
JavaScript 中大量使用对象来表示数据(内置对象与自定义对象遍布标准库与宿主 API),这为程序员提供了灵活性和便利性。因为可以很方便地、按照需要随时创造数据结构,不用进行麻烦的预定义。标准还在快速进化中,并不断合理化,添加更适用的语法特性。
JavaScript 语言本身是解释型语言,但是在现代浏览器中,JavaScript 都是编译后运行。程序会被高度优化,运行效率接近二进制程序。而且 JavaScript 引擎正在快速发展,性能将越来越好。
现代 JavaScript 引擎:
| 引擎 | 使用环境 | 特点 |
|---|---|---|
| V8 | Chrome、Node.js、Edge、Deno、Bun | 最快的引擎之一,使用 JIT 编译技术 |
| SpiderMonkey | Firefox | Mozilla 开发,是第一个 JavaScript 引擎 |
| JavaScriptCore | Safari、React Native | 苹果开发,也称为 Nitro / SquirrelFish |
| Hermes | React Native | Meta 专为 React Native 优化,预编译字节码 |
| QuickJS | 嵌入式环境 | 轻量级引擎,适合小型设备 |
| Engine262 | 测试/规范验证 | 用 JavaScript 实现的规范参考引擎 |
此外还有一种 WebAssembly 格式,它是一种可移植的二进制指令格式,可作为 JavaScript 引擎的编译目标。由于代码以预先编译好的二进制形式分发,省去了源码解析和逐步即时编译的开销,可以达到接近原生代码的运行速度。各种语言(主要是 C 和 C++)通过编译成 WebAssembly,就可以在浏览器里面运行。
JavaScript 程序可以采用事件驱动(event-driven)和非阻塞式(non-blocking)设计,在服务器端适合高并发环境,普通的硬件就可以承受很大的访问量。
优势:
1996 年 8 月,微软模仿 JavaScript 开发了一种相近的语言,取名为 JScript,内置于 IE 3.0。Netscape 公司面临丧失浏览器脚本语言的主导权的局面
1996 年 11 月,Netscape 公司决定将 JavaScript 提交给国际标准化组织 ECMA,希望 JavaScript 能够成为国际标准,以此抵抗微软。
1997 年 7 月,ECMA 组织发布 262 号标准文件(ECMA-262)的第一版,规定了浏览器脚本语言的标准,并将这种语言称为 ECMAScript。因为这门语言的制定者是 ECMA,不是 Netscape,这样有利于保证这门语言的开放性和中立性
ECMA-262 标准后来也被另一个国际标准化组织 ISO(International Organization for Standardization)批准,标准号是 ISO-16262
1997 年 7 月 ECMAScript 1.0 发布
1998 年 6 月,ECMAScript 2.0 版发布
1999 年 12 月,ECMAScript 3.0 版发布,成为 JavaScript 的通行标准,得到了广泛支持
2007 年 10 月,ECMAScript 4.0 版草案发布,对 3.0 版做了大幅升级。草案发布后,由于 4.0 版的目标过于激进,各方对于是否通过这个标准,发生了严重分歧。ECMA 开会决定,中止 ECMAScript 4.0 的开发(即废除了这个版本),将其中涉及现有功能改善的一小部分,发布为 ECMAScript 3.1,不久后 ECMAScript 3.1 就改名为 ECMAScript 5
2009 年 12 月,ECMAScript 5.0 版 正式发布
2011 年 6 月,ECMAScript 5.1 版发布,并且成为 ISO 国际标准(ISO/IEC 16262:2011)。到了 2012 年底,所有主要浏览器都支持 ECMAScript 5.1 版的全部功能
2015 年 6 月,ECMAScript 6 正式发布,并且更名为"ECMAScript 2015"(ES2015)。这是因为 TC39 委员会计划,以后每年发布一个 ECMAScript 的版本。
| 版本 | 发布时间 | 主要特性 |
|---|---|---|
| ES2015 (ES6) | 2015 年 6 月 | let/const、箭头函数、类、模块、Promise、解构赋值、模板字符串等 |
| ES2016 (ES7) | 2016 年 6 月 | Array.prototype.includes()、指数运算符 (**) |
| ES2017 (ES8) | 2017 年 6 月 | async/await、Object.entries()、Object.values()、字符串填充方法 |
| ES2018 (ES9) | 2018 年 6 月 | 异步迭代器、Rest/Spread 属性、正则表达式改进 |
| ES2019 (ES10) | 2019 年 6 月 | Array.flat()、Array.flatMap()、Object.fromEntries()、String.trimStart()/trimEnd() |
| ES2020 (ES11) | 2020 年 6 月 | BigInt、动态 import()、globalThis、可选链 (?.)、空值合并 (??) |
| ES2021 (ES12) | 2021 年 6 月 | String.replaceAll()、Promise.any()、逻辑赋值运算符 (??=、&&=、||=) |
| ES2022 (ES13) | 2022 年 6 月 | 类字段声明、私有方法和字段、Top-level await、Object.hasOwn() |
| ES2023 (ES14) | 2023 年 6 月 | Array.findLast()、Array.findLastIndex()、Array.toSorted()、Array.toReversed() 等 |
| ES2024 (ES15) | 2024 年 6 月 | Object.groupBy()、Promise.withResolvers()、RegExp v 标志、ArrayBuffer 转移 |
| ES2025 (ES16) | 2025 年 6 月 | Iterator Helpers、Set Methods、Import Attributes、Promise.try()、Float16Array、RegExp.escape() |
重要说明:
Object.groupBy() 等实用方法,Promise.withResolvers() 简化 Promise 创建JavaScript 的含义却比 ECMA-262 中规定的要多得多。一个完整的 JavaScript 实现应该由下列三个不同的部分组成:核心(ECMAScript)、文档对象模型(DOM)、浏览器对象模型(BOM)
由 ECMA-262 定义的 ECMAScript 与 Web 浏览器没有依赖关系。实际上,这门语言本身并不包含输入和输出定义。ECMA-262 定义的只是这门语言的基础,而在此基础之上可以构建更完善的脚本语言。常见的 Web 浏览器只是 ECMAScript 实现可能的宿主环境之一
宿主环境不仅提供基本的 ECMAScript 实现,同时也会提供该语言的扩展,以便语言与环境之间对接交互。而这些扩展——如 DOM,则利用 ECMAScript 的核心类型和语法提供更多更具体的功能,以便实现针对环境的操作
ECMA-262 标准没有参照 Web 浏览器,但规定了这门语言的下列组成部分:
文档对象模型是针对 XML 但经过扩展用于 HTML 的应用程序编程接口。DOM 把整个页面映射为一个多层节点结构。HTML 或 XML 页面中的每个组成部分都是某种类型的节点,这些节点又包含着不同类型的数据
浏览器对象模型 Browser Object Model,从根本上讲 BOM 只处理浏览器窗口和框架;但习惯上也把所有针对浏览器的 JavaScript 扩展算作 BOM 的一部分。下面就是一些这样的扩展:
navigator 对象location 对象screen 对象cookies 的支持XMLHttpRequest 和 IE 的 ActiveXObject 这样的自定义对象现代 JavaScript 引擎的工作流程如下:
JavaScript 源码
↓
解析器(Parser) → 生成抽象语法树(AST)
↓
解释器(Interpreter) → 生成字节码
↓
JIT 编译器 → 优化编译热点代码
↓
执行机器码关键组件:
解析器(Parser):
解释器(Interpreter):
JIT 编译器:
垃圾回收器(Garbage Collector):
JavaScript 是单线程语言,通过事件循环(Event Loop)实现异步操作。
┌─────────────────────────────────────┐
│ 调用栈(Call Stack) │
│ ┌─────────────────────────────┐ │
│ │ 函数调用 │ │
│ │ - 同步代码 │ │
│ │ - 先进后出(LIFO) │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ Web APIs (浏览器提供) │
│ - DOM API │
│ - setTimeout / setInterval │
│ │ 微任务队列(Micro Task Queue) │ │
│ │ - Promise.then/catch/finally │ │
│ │ - process.nextTick (Node) │ │
│ │ - MutationObserver │ │
│ └────────────────────────────────┘ │
└─────────────────────────────────────┘事件循环的执行顺序:
示例:
console.log("1. 同步代码开始")
setTimeout(() => {
console.log("2. 宏任务 - setTimeout")
}, 0)
Promise.resolve()
.then(() => {
console.log("3. 微任务 - Promise")
})
.then(() => {
console.log("4. 微任务 - Promise chain")
})
console.log("5. 同步代码结束")
// 执行结果:
// 1. 同步代码开始
// 5. 同步代码结束
// 3. 微任务 - Promise
// 4. 微任务 - Promise chain
// 2. 宏任务 - setTimeout| 特性 | 宏任务(Macro Task) | 微任务(Micro Task) |
|---|---|---|
| 包含方法 | setTimeout、setInterval、I/O、UI 渲染 | Promise、process.nextTick、MutationObserver |
| 执行时机 | 每次事件循环执行一个 | 每次调用栈清空后执行所有 |
| 优先级 | 较低 | 较高 |
| 执行特点 | 每次只执行一个 | 一次性执行完队列中所有任务 |
将 JavaScript 插入 HTML 的主要方法是使用 <script> 元素,这是由网景公司创造出来,后来被正式加入到 HTML 规范。script 元素有下列 8 个属性:
| 属性 | 说明 |
|---|---|
| async(可选) | 表示应该立即下载脚本,但不应妨碍页面中的其他操作,比如下载其他资源或等待加载其他脚本。只对外部脚本文件有效 |
| charset(可选) | 表示通过 src 属性指定的代码的字符集。由于大多数浏览器会忽略它的值,因此这个属性很少有人用 |
| crossorigin(可选) | 配置相关请求的 CORS(跨源资源共享)设置。默认不使用 CORS。crossorigin= "anonymous" 配置文件请求不必设置凭据标志。crossorigin="use-credentials" 设置凭据标志,意味着出站请求会包含凭据 |
| defer(可选) | 表示脚本可以延迟到文档完全被解析和显示之后再执行。只对外部脚本文件有效 |
| integrity(可选) | 允许比对接收到的资源和指定的加密签名以验证子资源完整性(SRI,Subresource Integrity)。如果接收到的资源的签名与这个属性指定的签名不匹配,则页面会报错,脚本不会执行。这个属性可以用于确保内容分发网络(CDN,Content Delivery Network)不会提供恶意内容 |
| language(已废弃) | 原来用于表示编写代码使用的脚本语言(如 JavaScript、JavaScript1.2 或 VBScript)。大多数浏览器会忽略这个属性,因此也没有必要再用 |
| src(可选) | 表示包含要执行代码的外部文件 |
| type(可选) | type 可以看成是 language 的替代属性;表示编写代码使用的脚本语言的内容类型(也称为 MIME 类型)。目前 type 属性的值依旧还是 text/javascript。不过这个属性并不是必需的,如果没有指定这个属性,则其默认值仍为 text/javascript。如果这个值是 module,则代码会被当成 ES6 模块,而且只有这时候代码中才能出现 import 和 export 关键字 |
使用 script 元素的方式有两种:直接在页面中嵌入 JavaScript 代码和包含外部 JavaScript文件。在使用 <script> 嵌入 JavaScript 代码时,不要在代码中的任何地方出现 </script> 字符串。而通过转义字符 \ 可以解决这个问题
<script type="text/javascript">
// ✅ 正确的行内脚本写法
function sayHi() {
alert("Hi!")
}
// ✅ 使用转义字符避免闭合标签问题
function sayScript() {
// 不能直接写 </script>,需要转义
alert("<\/script>") // 使用 \/ 转义斜杠
}
// 调用函数
sayHi() // 弹出 "Hi!"
sayScript() // 弹出 "</script>"
</script>如果要通过<script>元素来包含外部 JavaScript 文件,使用 src 属性指向外部 JavaScript 文件的链接,例如:
<!-- 方式1:引用本地文件 -->
<script type="text/javascript" src="example.js"></script>
<!-- 方式2:引用 CDN 文件 -->
<script src="https://cdn.example.com/library.min.js"></script>
<!-- 方式3:使用 integrity 属性验证文件完整性 -->
<script
src="https://cdn.example.com/library.min.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/ux..."
crossorigin="anonymous"></script>传统做法,所有 <script> 元素都应该放在页面的 <head>元素中
把所有外部文件(包括 CSS 文件和 JavaScript 文件)的引用都放在相同的地方。但在文档的
<head>元素中包含所有 JavaScript 文件,意味着必须等到全部 JavaScript 代码都被下载、解析和执行完成以后,才能开始呈现页面的内容(浏览器在遇到<body>标签时才开始呈现内容)
<!DOCTYPE html>
<html>
<head>
<title>Example HTML Page</title>
<script type="text/javascript" src="example1.js"></script>
<script type="text/javascript" src="example2.js"></script>
</head>
<body>
<!-- 这里放内容 -->
</body>
</html>这样在解析包含的 JavaScript 代码之前,页面的内容将完全呈现在浏览器中。而用户也会因为浏览器窗口显示空白页面的时间缩短而感到打开页面的速度加快
defer 属性用途是表明脚本在执行时不会影响页面的构造。也就是说,脚本会被延迟到整个页面都解析完毕后再运行。因此在 <script> 元素中设置 defer 属性,相当于告诉浏览器立即下载,但延迟执行
<!DOCTYPE html>
<html>
<head>
<title>Example HTML Page</title>
<!-- defer 脚本会立即下载,但延迟到 DOM 解析完成后执行 -->
<script type="text/javascript" defer="defer" src="example1.js"></script>
<script type="text/javascript" defer="defer" src="example2.js"></script>
</head>
<body>
<!-- 这里放内容 -->
</body>
</html>执行顺序说明:
页面加载流程:
1. 开始解析 HTML
2. 遇到 defer 脚本 → 立即下载,但不阻塞解析
3. 继续解析 HTML,构建 DOM 树
4. DOM 解析完成(DOMContentLoaded 事件触发前)
5. 按照 defer 脚本在文档中的顺序执行:
- example1.js 执行
- example2.js 执行虽然把 <script> 元素放在文档的 <head> 元素中,但其中包含的脚本将延迟到浏览器遇到 </html> 标签后再执行。
HTML5 规范要求脚本按照它们出现的先后顺序执行,因此第一个延迟脚本会先于第二个延迟脚本执行,而这两个脚本会先于 DOMContentLoaded 事件执行。在现实当中,延迟脚本并不一定会按照顺序执行,也不一定会在 DOMContentLoaded 事件触发前执行,因此最好只包含一个延迟脚本
defer 使用场景:
// ✅ 适合使用 defer 的场景
// 1. 操作 DOM 的脚本
// 2. 依赖 DOM 结构的初始化代码
// 3. 需要按顺序执行的脚本
// ❌ 不适合使用 defer 的场景
// 1. 页面渲染前必须执行的代码
// 2. document.write() 操作(不推荐使用)async 属性与 defer 属性类似,都用于改变处理脚本的行为。与 defer 类似,async 只适用于外部脚本文件,并告诉浏览器立即下载文件。但与 defer 不同的是,标记为 async 的脚本并不保证按照指定它们的先后顺序执行
<!DOCTYPE html>
<html>
<head>
<title>Example HTML Page</title>
<!-- async 脚本会在下载完成后立即执行,不保证执行顺序 -->
<script type="text/javascript" async src="example1.js"></script>
<script type="text/javascript" async src="example2.js"></script>
</head>
<body>
<!-- 这里放内容 -->
</body>
</html>执行顺序说明:
页面加载流程:
1. 开始解析 HTML
2. 遇到 async 脚本 → 立即下载,不阻塞解析
3. 继续解析 HTML
4. 哪个脚本先下载完成,就先执行哪个(不保证顺序)
可能的执行顺序:
- example2.js 先下载完成 → 立即执行
- example1.js 后下载完成 → 立即执行
或者反过来上面代码中,第二个脚本文件可能会在第一个脚本文件之前执行。因此确保两者之间互不依赖非常重要。
指定 async 属性的目的是不让页面等待两个脚本下载和执行,从而异步加载页面其他内容。为此,建议异步脚本不要在加载期间修改 DOM。异步脚本一定会在页面的 load 事件前执行,但可能会在 DOMContentLoaded 事件触发之前或之后执行
async 使用场景:
// ✅ 适合使用 async 的场景
// 1. 独立的第三方库(如统计代码、广告脚本)
// 2. 不依赖 DOM 和其他脚本的代码
// 示例:加载 Google Analytics
// <script async src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"></script>
// ❌ 不适合使用 async 的场景
// 1. 依赖其他脚本的代码
// 2. 依赖 DOM 的初始化代码
// 3. 需要按顺序执行的模块defer vs async 对比:
普通脚本(无属性):
[解析 HTML] → [暂停] → [下载脚本] → [执行脚本] → [继续解析 HTML]
❌ 阻塞页面渲染
defer 脚本:
[解析 HTML] ←→ [下载脚本(并行)]
↓
[DOM 解析完成]
↓
[执行 defer 脚本(按顺序)]
✅ 不阻塞,按顺序执行
async 脚本:
[解析 HTML] ←→ [下载脚本(并行)]
↓ ↓
[继续解析] [下载完成立即执行(不保证顺序)]
✅ 不阻塞,独立执行因为 JavaScript 可以使用 DOM API,通过向 DOM 中动态添加 script 元素同样可以加载指定的脚本。只要创建一个 script 元素并将其添加到 DOM 即可
let script = document.createElement("script")
script.src = "gibberish.js"
document.head.appendChild(script)⚠️ 默认情况下这种方式创建的
<script>元素是以异步方式加载的,相当于添加了async属性。不过这样做可能会有问题,因为所有浏览器都支持createElement()方法,但不是所有浏览器都支持async属性。因此要统一动态脚本的加载行为,可以明确将其设置为同步加载:
let script = document.createElement("script")
script.src = "gibberish.js"
script.async = false
document.head.appendChild(script)以这种方式获取的资源对浏览器预加载器是不可见的。严重影响它们在资源获取队列中的优先级。根据应用程序的工作方式以及怎么使用,这种方式可能会严重影响性能。要想让预加载器知道这些动态请求文件的存在,可以在文档头部显式声明它们:
<link rel="preload" href="gibberish.js" />可扩展超文本标记语言(XHTML,Extensible HyperText Markup Language)是将 HTML 作为 XML 的应用重新包装的结果。在 XHTML 中使用 JavaScript 必须指定 type 属性且值为 text/javascript。XHTML 虽然已经退出历史舞台,但实践中偶尔可能也会遇到遗留代码
在 XHTML 中编写代码的规则比 HTML 中严格。a < b 语句中的小于号会被解释成标签的开始,导致语法错误。
避免方法:将小于号替换为 < 实体,或将代码包含到 CDATA 块中并用 JavaScript 注释抵消:
<script type="text/javascript">
//<![CDATA[
function compare(a, b) {
if (a < b) { console.log("A is less than B") }
else if (a > b) { console.log("A is greater than B") }
else { console.log("A is equal to B") }
}
//]]>
</script>在 HTML 中嵌入 JavaScript 代码虽然没有问题,但最好的做法还是尽可能使用外部文件来包含 JavaScript 代码。不过,并不存在必须使用外部文件的硬性规定,但支持使用外部文件的人多会强调如下优点:
使用外部文件的优势:
HTML 页面的 JavaScript 会造成维护问题。将代码集中到外部文件中,更容易维护和更新JavaScript 文件。如果有两个页面都使用同一个文件,那么这个文件只需下载一次何时使用行内代码:
从 ES2015 开始,JavaScript 支持原生模块系统。使用 type="module" 属性可以启用 ES6 模块:
<!-- 使用 ES6 模块 -->
<script type="module" src="main.js"></script>
<!-- 内联模块代码 -->
<script type="module">
import { helper } from "./helper.js"
helper()
</script>ES6 模块的特点:
import 和 exportdefer)模块示例:
// helper.js
export function helper() { console.log("Helper function") }
export const API_URL = "https://api.example.com"
// main.js
import { helper, API_URL } from "./helper.js"
helper()推荐的脚本加载顺序:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>页面标题</title>
<!-- 关键 CSS -->
<link rel="stylesheet" href="critical.css" />
<!-- 预加载重要资源 -->
<link rel="preload" href="main.js" as="script" />
</head>
<body>
<!-- 页面内容 -->
<!-- 非关键脚本放在 body 末尾 -->
<script src="main.js"></script>
<!-- 或者使用 defer -->
<!-- <script defer src="main.js"></script> -->
</body>
</html>性能优化建议:
defer:需要按顺序执行且不阻塞页面渲染async:不依赖其他脚本,可以异步加载preload、prefetch 等文档模式是通过使用文档类型(doctype)切换实现的。最初的两种文档模式是:混杂模式(quirks mode)和标准模式(standards mode)
JavaScript 的解释执行。本书将在必要时再讨论这些因文档模式而影响 JavaScript 执行的情况如果在文档开始处没有发现文档类型声明,则所有浏览器都会默认开启混杂模式。但采用混杂模式不是什么值得推荐的做法,因为不同浏览器在这种模式下的行为差异非常大,如果不使用某些 hack 技术,跨浏览器的行为根本就没有一致性可言。
对于标准模式,可以通过使用 <!DOCTYPE html>(HTML5)来开启。而对于准标准模式,则可以通过使用过渡型(transitional)或框架集型(frameset)文档类型来触发。
准标准模式与标准模式非常接近,它们的差异几乎可以忽略不计。因此,当有人提到“标准模式”时,有可能是指这两种模式中的任何一种
早期浏览器都面临一个特殊的问题,即当浏览器不支持 JavaScript 时如何让页面平稳地退化。最终解决方案是创造<noscript> 元素,用以在不支持 JavaScript 的浏览器中显示替代的内容。这个元素可以包含能够出现在文档 <body> 中的任何 HTML 元素——<script> 元素除外。包含在 <noscript> 元素中的内容只有在下列情况下才会显示出来:
符合上述任何一个条件,浏览器都会显示 <noscript> 中的内容。而在除此之外的其他情况下,浏览器不会呈现 <noscript> 中的内容
<html>
<head>
<title>Example HTML Page</title>
<script type="text/javascript" defer="defer" src="example1.js"></script>
<script type="text/javascript" defer="defer" src="example2.js"></script>
</head>
<body>
<noscript> <p>本页面需要浏览器支持(启用)JavaScript。</p></noscript>
</body>
</html>这个页面会在脚本无效的情况下向用户显示一条消息。而在启用了脚本的浏览器中,用户永远也不会看到它——尽管它是页面的一部分
现代用法示例:
<noscript>
<div style="padding: 20px; background: #f0f0f0; border: 1px solid #ccc;">
<h2>需要启用 JavaScript</h2>
<p>此页面需要 JavaScript 才能正常工作。请启用浏览器的 JavaScript 功能。</p>
</div>
</noscript>现代 JavaScript 开发离不开各种框架和库,它们大大提高了开发效率:
主流前端框架:
1. 减少 DOM 操作
// ❌ 避免:频繁的 DOM 操作
for (let i = 0; i < 1000; i++) {
const div = document.createElement("div")
document.body.appendChild(div) // 每次都触发重排
}
// ✅ 推荐:使用文档片段批量操作
const fragment = document.createDocumentFragment()
for (let i = 0; i < 1000; i++) {
const div = document.createElement("div")
fragment.appendChild(div)
}
document.body.appendChild(fragment) // 只触发一次重排2. 避免内存泄漏
// ❌ 常见内存泄漏:未清理的事件监听器
class Component {
constructor() {
this.handleClick = this.handleClick.bind(this)
document.addEventListener("click", this.handleClick)
}
// ✅ 必须在组件销毁时清理
destroy() {
document.removeEventListener("click", this.handleClick)
}
}3. 使用防抖和节流
// 防抖(Debounce)
function debounce(func, delay) {
let timer = null
return function (...args) {
clearTimeout(timer)
timer = setTimeout(() => func.apply(this, args), delay)
}
}
// 节流(Throttle)
function throttle(func, delay) {
let lastTime = 0
return function (...args) {
const now = Date.now()
if (now - lastTime >= delay) {
func.apply(this, args)
lastTime = now
}
}
}1. 使用严格模式
"use strict"
// 严格模式捕获常见错误、禁止 this 指向全局对象、更容易优化2. 变量声明最佳实践
// ✅ 优先使用 const
const MAX_SIZE = 100
const config = { debug: true }
// ✅ 需要重新赋值时使用 let
let count = 0
count++
// ❌ 避免使用 var
var x = 1 // 函数作用域,容易导致问题3. 函数设计原则
// ✅ 单一职责:一个函数只做一件事
function validateEmail(email) {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return regex.test(email)
}
// ✅ 纯函数:相同输入总是产生相同输出
function add(a, b) {
return a + b
}4. 错误处理
async function fetchData(url) {
try {
const response = await fetch(url)
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
return await response.json()
} catch (error) {
console.error("获取数据失败:", error)
throw error
}
}1. 使用可选链和空值合并
const user = { profile: { name: "张三" } }
// ✅ 可选链操作符 ?.
const city = user?.profile?.address?.city // undefined(不会报错)
// ✅ 空值合并运算符 ??
const displayCity = city ?? "未知城市" // "未知城市"
// 与 || 的区别:空字符串用 ?? 不会被当作 falsy2. 使用展开运算符
const arr1 = [1, 2, 3]
const arr2 = [4, 5, 6]
const combined = [...arr1, ...arr2] // [1, 2, 3, 4, 5, 6]
const defaults = { theme: "light", lang: "zh" }
const userConfig = { lang: "en" }
const config = { ...defaults, ...userConfig } // { theme: 'light', lang: 'en' }3. 模块化开发
// utils.js
export const PI = 3.14159
export function calculateArea(radius) { return PI * radius * radius }
export default class Calculator {}
// main.js
import Calculator, { PI, calculateArea } from "./utils.js"
// 动态导入(按需加载)
const module = await import("./heavy-module.js")Q: var、let 和 const 有什么区别?
// var:函数作用域,存在变量提升
console.log(a) // undefined
var a = 1
// let:块级作用域,暂时性死区
// console.log(b) // ReferenceError
let b = 2
// const:块级作用域,必须初始化,不能重新赋值(但对象属性可修改)
const c = 3Q: == 和 === 有什么区别?
// ==:宽松相等,会进行类型转换
1 == "1" // true
null == undefined // true
// ===:严格相等,不进行类型转换
1 === "1" // false
null === undefined // false
// ✅ 推荐:始终使用 ===Q: 什么是闭包?
function createCounter() {
let count = 0
return {
increment() { count++; return count },
getCount() { return count }
}
}
const counter = createCounter()
console.log(counter.increment()) // 1Q: Promise、async/await 和回调函数的区别?
// 回调函数(Callback)— ❌ 回调地狱
fetchData(function(data) {
processData(data, function(result) {
displayResult(result, function() { console.log("完成") })
})
})
// Promise — ✅ 链式调用
fetchData()
.then(data => processData(data))
.then(result => displayResult(result))
.catch(error => console.error(error))
// async/await — ✅ 同步风格的异步代码
async function handleData() {
try {
const data = await fetchData()
const result = await processData(data)
await displayResult(result)
} catch (error) { console.error(error) }
}Q: 如何处理多个异步操作?
// 并行执行
const [users, products] = await Promise.all([fetchUsers(), fetchProducts()])
// 竞争执行
const result = await Promise.race([fetchFromServer1(), fetchFromServer2()])Q: 如何优化 JavaScript 性能?
// 1. 减少重排和重绘:使用 cssText 批量修改样式
element.style.cssText = "width: 100px; height: 100px; background-color: red;"
// 2. 使用事件委托
document.getElementById("list").addEventListener("click", (e) => {
if (e.target.classList.contains("item")) handleClick(e)
})
// 3. 懒加载
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.src = entry.target.dataset.src
observer.unobserve(entry.target)
}
})
})Q: 什么是内存泄漏?如何避免?
// 1. 意外的全局变量
function leak() { bar = "I'm global now" } // ❌
// 2. 未清理的定时器
class Timer {
constructor() { this.timer = setInterval(() => {}, 1000) }
destroy() { clearInterval(this.timer) } // ✅ 必须清理
}
// 3. 闭包引用大对象 — 只保存需要的值Q: 如何高效调试 JavaScript?
// 常用 console 方法
console.table([{ name: "张三", age: 25 }, { name: "李四", age: 30 }])
console.group("分组"); console.log("内容"); console.groupEnd()
// debugger 语句:代码会在此处暂停
function debugExample() { const data = getData(); debugger; processData(data) }
// 查看调用栈
console.trace()Q: 如何处理浏览器兼容性?
// 1. 特性检测
if ("IntersectionObserver" in window) {
const observer = new IntersectionObserver(callback)
} else {
window.addEventListener("scroll", throttle(callback, 100))
}
// 2. Polyfill + Babel 等转译工具将 ES6+ 转换为兼容代码记住:扎实的基础是成为优秀 JavaScript 开发者的关键!