{T}

JavaScript 概述

基本介绍

JavaScript 是一种轻量级的脚本语言,脚本语言指的是它不具备开发操作系统的能力,而是只用来编写控制其他大型应用程序(比如浏览器)

⚠️ JavaScript 也是一种嵌入式语言,本身提供的核心语法不算多,只能用来做一些数学和逻辑运算。并且本身不提供任何与 I/O(输入/输出)相关的 API,都要靠宿主环境提供,所以 JavaScript 只合适嵌入更大型的应用程序环境,去调用宿主环境提供的底层 API 目前已经嵌入 JavaScript 的宿主环境有多种,最常见的环境就是浏览器,另外还有服务器环境,也就是 Node 项目 从语法角度看,JavaScript 语言是一种“对象模型”语言。各种宿主环境通过这个模型,描述自己的功能和操作接口,从而通过 JavaScript 控制这些功能。但是 JavaScript 并不是纯粹的“面向对象语言”,还支持其他编程范式(比如函数式编程)。这导致几乎任何一个问题,JavaScript 都有多种解决方法

JavaScript 是门用来与网页交互的脚本语言,包含以下三个组成部分:

  • ECMAScript:由 ECMA-262 定义并提供核心功能
  • 文档对象模型(DOM):提供与网页内容交互的方法和接口
  • 浏览器对象模型(BOM):提供与浏览器交互的方法和接口

核心语法

JavaScript 核心语法部分相当精简,只包括两个部分:

  • 基本的语法构造(比如操作符、控制结构、语句)
  • 标准库(就是一系列具有各种功能的对象比如ArrayDateMath 等)

除此之外,各种宿主环境提供额外的 APIJavaScript 调用。以浏览器为例,它提供的额外 API 可以分成三大类:

  • 浏览器控制类:操作浏览器
  • DOM 类:操作网页的各种元素
  • Web 类:实现互联网的各种功能

如果宿主环境是服务器,则会提供各种操作系统的 API,比如文件操作 API、网络通信 API 等等

主要介绍 JavaScript 核心语法和浏览器网页开发的基本知识,不涉及 Node

  • 基本语法
  • 标准库
  • 浏览器 API
  • DOM

操控浏览器的能力

JavaScript 发明目的,就是作为浏览器的内置脚本语言,为网页开发者提供操控浏览器的能力。它是目前唯一一种通用的浏览器脚本语言,所有浏览器都支持。它可以让网页呈现各种特殊效果,为用户提供良好的互动体验。

目前全世界几乎所有网页都使用 JavaScript。如果不用,网站的易用性和使用效率将大打折扣,无法成为操作便利、对用户友好的网站

广泛的使用领域

1. 浏览器的平台化

随着 HTML5 的出现,浏览器本身的功能越来越强,不再仅仅能浏览网页,而是越来越像一个平台,JavaScript 因此得以调用许多系统功能,比如操作本地文件、操作图片、调用摄像头和麦克风等等。这使得 JavaScript 可以完成许多以前无法想象的事情

2. Node

Node 项目使得 JavaScript 可以用于开发服务器端的大型项目,网站的前后端都用 JavaScript 开发已经成为了现实。有些嵌入式平台(Raspberry Pi)能够安装 Node,于是 JavaScript 就能为这些平台开发应用程序

2.1 Deno

Deno 是由 Node.js 原作者 Ryan Dahl 于 2018 年创建的新 JavaScript/TypeScript 运行时,旨在解决 Node.js 的设计缺陷:

  • 安全优先:默认沙箱机制,文件/网络/环境访问需显式授权
  • 原生 TypeScript:无需配置即可直接运行 .ts 文件
  • 标准库完善:内置 HTTP 服务器、文件操作、加密等常用 API
  • Web API 兼容:实现了 fetchWebSocketURL 等浏览器标准 API
  • ES Module 原生支持:使用 import/export 而非 require()

2.2 Bun

Bun 是 2022 年推出的高性能 JavaScript/TypeScript 运行时,使用 JavaScriptCore 引擎:

  • 极致性能:启动速度和包安装速度远超 Node.js 和 Deno
  • 全能工具链:集成运行时、包管理器、构建工具、测试框架
  • Node.js 兼容:支持绝大多数 Node.js API 和 npm 包
  • 原生支持:TypeScript、JSX、TSX 开箱即用
  • 内置 APIBun.file()Bun.serve()Bun.password 等高效内置方法

3. 数据库操作

JavaScript 甚至也可以用来操作数据库。NoSQL 数据库这个概念,本身就是在 JSON 格式的基础上诞生的,大部分 NoSQL 数据库允许 JavaScript 直接操作。基于 SQL 语言的开源数据库 PostgreSQL 支持 JavaScript 作为操作语言,可以部分取代 SQL 查询语言

4. 移动平台开发

JavaScript 也正在成为手机应用的开发语言。一般来说,安卓平台使用 Java 语言开发,iOS 平台使用 Objective-CSwift 语言开发。许多人正在努力,让 JavaScript 成为各个平台的通用开发语言

PhoneGap 项目就是将 JavaScriptHTML5 打包在一个容器之中,使得它能同时在 iOS 和安卓上运行。Facebook 公司的 React Native 项目则是将 JavaScript 写的组件,编译成原生组件,从而使它们具备优秀的性能。此外,Expo 基于 React Native 提供了更简化的开发体验,Capacitor(Ionic 团队)则是 PhoneGap 的现代替代方案

5. 内嵌脚本语言

越来越多的应用程序,将 JavaScript 作为内嵌的脚本语言,比如 Adobe 公司的著名 PDF 阅读器 AcrobatLinux 桌面环境 GNOME 3

6. 跨平台的桌面应用程序

Chromium OSWindows 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 的性能优势体现在以下方面:

1. 灵活的语法,表达力强

JavaScript 既支持类似 C 语言清晰的过程式编程,也支持灵活的函数式编程,可以用来写并发处理(concurrent)。这些语法特性已经被证明非常强大,可以用于许多场合,尤其适用异步编程。

JavaScript 的所有值都是对象,这为程序员提供了灵活性和便利性。因为可以很方便地、按照需要随时创造数据结构,不用进行麻烦的预定义。标准还在快速进化中,并不断合理化,添加更适用的语法特性。

2. 支持编译运行

JavaScript 语言本身是解释型语言,但是在现代浏览器中,JavaScript 都是编译后运行。程序会被高度优化,运行效率接近二进制程序。而且 JavaScript 引擎正在快速发展,性能将越来越好。

现代 JavaScript 引擎:

引擎使用环境特点
V8Chrome、Node.js、Edge、Deno、Bun最快的引擎之一,使用 JIT 编译技术
SpiderMonkeyFirefoxMozilla 开发,是第一个 JavaScript 引擎
JavaScriptCoreSafari、React Native苹果开发,也称为 Nitro / SquirrelFish
HermesReact NativeMeta 专为 React Native 优化,预编译字节码
QuickJS嵌入式环境轻量级引擎,适合小型设备
Engine262测试/规范验证用 JavaScript 实现的规范参考引擎

此外还有一种 WebAssembly 格式,它是 JavaScript 引擎的中间码格式,全部都是二进制代码。由于跳过了编译步骤,可以达到接近原生二进制代码的运行速度。各种语言(主要是 C 和 C++)通过编译成 WebAssembly,就可以在浏览器里面运行。

3. 事件驱动和非阻塞式设计

JavaScript 程序可以采用事件驱动(event-driven)和非阻塞式(non-blocking)设计,在服务器端适合高并发环境,普通的硬件就可以承受很大的访问量。

优势:

  • 单线程模型避免了线程同步的复杂性
  • 事件循环机制高效处理异步操作
  • 适合 I/O 密集型应用
  • 在 Node.js 中表现优异

与 ECMAScript 的关系

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

JavaScript 版本

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 的版本。

ECMAScript 版本历史(2015-2025)

版本发布时间主要特性
ES2015 (ES6)2015 年 6 月let/const、箭头函数、类、模块、Promise、解构赋值、模板字符串等
ES2016 (ES7)2016 年 6 月Array.prototype.includes()、指数运算符 (**)
ES2017 (ES8)2017 年 6 月async/awaitObject.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 awaitObject.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()Float16ArrayRegExp.escape()

重要说明:

  • ES2015(ES6)是一次重大更新,引入了许多现代 JavaScript 特性
  • 从 ES2016 开始,每年发布一个版本,采用年份命名
  • ES2024 引入了 Object.groupBy() 等实用方法,Promise.withResolvers() 简化 Promise 创建
  • ES2025 是一次较大更新,Iterator Helpers 和 Set Methods 大幅增强了集合操作能力
  • 现代浏览器和 Node.js 都支持这些新特性,但需要注意兼容性
  • 可以使用 Babel、SWC 等工具将新语法转换为旧版本,以支持旧浏览器

JavaScript 实现

JavaScript 的含义却比 ECMA-262 中规定的要多得多。一个完整的 JavaScript 实现应该由下列三个不同的部分组成核心:(ECMAScript)、文档对象模型(DOM)浏览器对象模型 (BOM)

ECMAScript

由 ECMA-262 定义的 ECMAScript 与 Web 浏览器没有依赖关系。实际上,这门语言本身并不包含输入和输出定义。ECMA-262 定义的只是这门语言的基础,而在此基础之上可以构建更完善的脚本语言。常见的 Web 浏览器只是 ECMAScript 实现可能的宿主环境之一

宿主环境不仅提供基本的 ECMAScript 实现,同时也会提供该语言的扩展,以便语言与环境之间对接交互。而这些扩展——如 DOM,则利用 ECMAScript 的核心类型和语法提供更多更具体的功能,以便实现针对环境的操作

ECMA-262 标准没有参照 Web 浏览器,但规定了这门语言的下列组成部分:

  • 语法
  • 类型
  • 语句
  • 关键字
  • 保留字
  • 操作符
  • 对象

文档对象模型 DOM

文档对象模型是针对 XML 但经过扩展用于 HTML 的应用程序编程接口。DOM 把整个页面映射为一个多层节点结构。HTML 或 XML 页面中的每个组成部分都是某种类型的节点,这些节点又包含着不同类型的数据

html
<!-- HTML 结构省略,仅展示关键 JS 逻辑 -->

浏览器对象模型 BOM

浏览器对象模型 Browser Object Model,从根本上讲 BOM 只处理浏览器窗口和框架;但习惯上也把所有针对浏览器的 JavaScript 扩展算作 BOM 的一部分。下面就是一些这样的扩展:

  • 弹出新浏览器窗口的功能

  • 移动、缩放和关闭浏览器窗口的功能

  • 提供浏览器详细信息的 navigator 对象

  • 提供浏览器所加载页面的详细信息的 location 对象

  • 提供用户显示器分辨率详细信息的 screen 对象

  • cookies 的支持

  • XMLHttpRequest 和 IE 的 ActiveXObject 这样的自定义对象

    // ... 中间省略 ...

| Hermes | React Native | Facebook 专为 React Native 优化 |

引擎工作原理

现代 JavaScript 引擎的工作流程如下:

code
JavaScript 源码
     ↓
解析器(Parser) → 生成抽象语法树(AST)
     ↓
解释器(Interpreter) → 生成字节码
     ↓
JIT 编译器 → 优化编译热点代码
     ↓
执行机器码

关键组件:

  1. 解析器(Parser)

    • 将源代码转换为抽象语法树(AST)
    • 进行词法分析和语法分析
    • 检查语法错误
  2. 解释器(Interpreter)

    • 快速执行代码,无需等待编译
    • 生成字节码(Bytecode)
    • V8 中称为 Ignition
  3. JIT 编译器

    • 识别热点代码(频繁执行的代码)
    • 将热点代码编译为优化的机器码
    • V8 中称为 TurboFan
  4. 垃圾回收器(Garbage Collector)

    • 自动管理内存
    • 回收不再使用的对象
    • 防止内存泄漏
code

### 事件循环机制

JavaScript 是单线程语言,通过事件循环(Event Loop)实现异步操作。

#### 执行栈与任务队列

┌─────────────────────────────────────┐ │ 调用栈(Call Stack) │ │ ┌─────────────────────────────┐ │ │ │ 函数调用 │ │ │ │ - 同步代码 │ │ │ │ - 先进后出(LIFO) │ │ │ └─────────────────────────────┘ │ └─────────────────────────────────────┘ ↓ ┌─────────────────────────────────────┐ │ Web APIs (浏览器提供) │ │ - DOM API │

// ... 中间省略 ...

│ │ 微任务队列(Micro Task Queue) │ │ │ │ - Promise.then/catch/finally │ │ │ │ - process.nextTick (Node) │ │ │ │ - MutationObserver │ │ │ └────────────────────────────────┘ │ └─────────────────────────────────────┘

code

#### 事件循环流程

事件循环的执行顺序:

1. 执行同步代码(调用栈)
2. 调用栈清空后,检查微任务队列
3. 执行所有微任务
4. 浏览器 UI 渲染
5. 执行一个宏任务
6. 回到第 2 步

**示例:**

```javascript
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
执行时机每次事件循环执行一个每次调用栈清空后执行所有
优先级较低较高
执行特点每次只执行一个一次性执行完队列中所有任务
code

## script 元素

将 `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(可选)        | t 可以看成是 language 的替代属性;表示编写代码使用的脚本语言的内容类型(也称为 MIME 类型)。目前 type 属性的值依旧还是 `text/javascript`。不过这个属性并不是必需的,如果没有指定这个属性,则其默认值仍为 text/javascript。如果这个值是 module,则代码会被当成 ES6 模块,而且只有这时候代码中才能出现 import 和 export 关键字 |

使用 `script` 元素的方式有两种:直接在页面中嵌入 `JavaScript` 代码和包含外部 `JavaScript`文件。在使用 `<script>` 嵌入 `JavaScript` 代码时,不要在代码中的任何地方出现 `</script>` 字符串。而通过转义字符 `/` 可以解决这个问题

```html
<script type="text/javascript">
  // ✅ 正确的行内脚本写法
  function sayHi() {
    alert("Hi!")
  }

  // ✅ 使用转义字符避免闭合标签问题
  function sayScript() {
    // 不能直接写 </script>,需要转义
    alert("<\/script>") // 使用 \/ 转义斜杠
  }

  // 调用函数
  sayHi() // 弹出 "Hi!"
  sayScript() // 弹出 "</script>"
</script>

如果要通过<script>元素来包含外部 JavaScript 文件,使用 src 属性指向外部 JavaScript 文件的链接,例如:

html
<!-- 方式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> 标签时才开始呈现内容)

html
<!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 立即下载但延迟执行

defer 属性用途是表明脚本在执行时不会影响页面的构造。也就是说,脚本会被延迟到整个页面都解析完毕后再运行。因此在 <script> 元素中设置 defer 属性,相当于告诉浏览器立即下载,但延迟执行

html
<!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>

执行顺序说明:

code
页面加载流程:
1. 开始解析 HTML
2. 遇到 defer 脚本 → 立即下载,但不阻塞解析
3. 继续解析 HTML,构建 DOM 树
4. DOM 解析完成(DOMContentLoaded 事件触发前)
5. 按照 defer 脚本在文档中的顺序执行:
   - example1.js 执行
   - example2.js 执行

虽然把 <script> 元素放在文档的 <head> 元素中,但其中包含的脚本将延迟到浏览器遇到 </html> 标签后再执行。

HTML5 规范要求脚本按照它们出现的先后顺序执行,因此第一个延迟脚本会先于第二个延迟脚本执行,而这两个脚本会先于 DOMContentLoaded 事件执行。在现实当中,延迟脚本并不一定会按照顺序执行,也不一定会在 DOMContentLoaded 事件触发前执行,因此最好只包含一个延迟脚本

defer 使用场景:

javascript
// ✅ 适合使用 defer 的场景
// 1. 操作 DOM 的脚本
// 2. 依赖 DOM 结构的初始化代码
// 3. 需要按顺序执行的脚本

// ❌ 不适合使用 defer 的场景
// 1. 页面渲染前必须执行的代码
// 2. document.write() 操作(不推荐使用)

async 异步执行脚本

async 属性与 defer 属性类似,都用于改变处理脚本的行为。与 defer 类似,async 只适用于外部脚本文件,并告诉浏览器立即下载文件。但与 defer 不同的是,标记为 async 的脚本并不保证按照指定它们的先后顺序执行

html
<!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>

执行顺序说明:

code
页面加载流程:
1. 开始解析 HTML
2. 遇到 async 脚本 → 立即下载,不阻塞解析
3. 继续解析 HTML
4. 哪个脚本先下载完成,就先执行哪个(不保证顺序)
   可能的执行顺序:
   - example2.js 先下载完成 → 立即执行
   - example1.js 后下载完成 → 立即执行
   或者反过来

上面代码中,第二个脚本文件可能会在第一个脚本文件之前执行。因此确保两者之间互不依赖非常重要。

指定 async 属性的目的是不让页面等待两个脚本下载和执行,从而异步加载页面其他内容。为此,建议异步脚本不要在加载期间修改 DOM。异步脚本一定会在页面的 load 事件前执行,但可能会在 DOMContentLoaded 事件触发之前或之后执行

async 使用场景:

javascript
// ✅ 适合使用 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 对比:

code
普通脚本(无属性):
[解析 HTML] → [暂停] → [下载脚本] → [执行脚本] → [继续解析 HTML]
❌ 阻塞页面渲染

defer 脚本:
[解析 HTML] ←→ [下载脚本(并行)]
     ↓
[DOM 解析完成]
     ↓
[执行 defer 脚本(按顺序)]
✅ 不阻塞,按顺序执行

async 脚本:
[解析 HTML] ←→ [下载脚本(并行)]
     ↓           ↓
[继续解析]   [下载完成立即执行(不保证顺序)]
✅ 不阻塞,独立执行

动态加载脚本

因为 JavaScript 可以使用 DOM API,通过向 DOM 中动态添加 script 元素同样可以加载指定的脚本。只要创建一个 script 元素并将其添加到 DOM 即可

javascript
let script = document.createElement("script")
script.src = "gibberish.js"
document.head.appendChild(script)

⚠️ 默认情况下这种方式创建的 <script> 元素是以异步方式加载的,相当于添加了 async 属性。不过这样做可能会有问题,因为所有浏览器都支持 createElement() 方法,但不是所有浏览器都支持 async 属性。因此要统一动态脚本的加载行为,可以明确将其设置为同步加载:

javascript
let script = document.createElement("script")
script.src = "gibberish.js"
script.async = false
document.head.appendChild(script)

以这种方式获取的资源对浏览器预加载器是不可见的。严重影响它们在资源获取队列中的优先级。根据应用程序的工作方式以及怎么使用,这种方式可能会严重影响性能。要想让预加载器知道这些动态请求文件的存在,可以在文档头部显式声明它们:

html
<link rel="preload" href="gibberish.js" />

XHTML 中的变化

可扩展超文本标记语言(XHTML,Extensible HyperText Markup Language)是将 HTML 作为 XML 的应用重新包装的结果。在 XHTML 中使用 JavaScript 必须指定 type 属性且值为 text/javascriptXHTML 虽然已经退出历史舞台,但实践中偶尔可能也会遇到遗留代码

XHTML 中编写代码的规则比 HTML 中严格。a < b 语句中的小于号会被解释成标签的开始,导致语法错误。

避免方法:将小于号替换为 &lt; 实体,或将代码包含到 CDATA 块中并用 JavaScript 注释抵消:

html
<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 文件。如果有两个页面都使用同一个文件,那么这个文件只需下载一次
  • 可复用:多个页面可以共享同一个 JavaScript 文件
  • 关注点分离:将 HTML、CSS 和 JavaScript 分离,使代码结构更清晰
  • 性能优化:可以更好地利用浏览器缓存和 CDN 加速

何时使用行内代码:

  • 页面特定的、一次性使用的代码
  • 需要立即执行的初始化代码
  • 非常小的代码片段(如配置数据)

ES6 模块

从 ES2015 开始,JavaScript 支持原生模块系统。使用 type="module" 属性可以启用 ES6 模块:

html
<!-- 使用 ES6 模块 -->
<script type="module" src="main.js"></script>

<!-- 内联模块代码 -->
<script type="module">
  import { helper } from "./helper.js"
  helper()
</script>

ES6 模块的特点:

  • 自动使用严格模式
  • 支持 importexport
  • 模块代码只执行一次
  • 模块是延迟执行的(类似 defer
  • 支持跨域加载(需要 CORS 配置)

模块示例:

javascript
// 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()

脚本加载最佳实践

推荐的脚本加载顺序:

html
<!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>

性能优化建议:

  1. 关键脚本使用 defer:需要按顺序执行且不阻塞页面渲染
  2. 独立脚本使用 async:不依赖其他脚本,可以异步加载
  3. 使用资源提示preloadprefetch
  4. 代码分割:将大型应用拆分为多个模块
  5. 懒加载:按需加载非关键代码

文档模式

文档模式是通过使用文档类型(doctype)切换实现的。最初的两种文档模式是:混杂模式(quirks mode)和标准模式(standards mode)

  • 混杂模式会让 IE 的行为与(包含非标准特性的)IE5 相同,而标准模式则让 IE 的行为更接近标准行为。虽然这两种模式主要影响 CSS 内容的呈现,但在某些情况下也会影响到 JavaScript 的解释执行。本书将在必要时再讨论这些因文档模式而影响 JavaScript 执行的情况
  • 在 IE 引入文档模式的概念后,其他浏览器也纷纷效仿。在此之后,IE 又提出一种所谓的准标准模式(almost standards mode)。这种模式下的浏览器特性有很多都是符合标准的,但也不尽然。不标准的地方主要体现在处理图片间隙的时候(在表格中使用图片时问题最明显)

如果在文档开始处没有发现文档类型声明,则所有浏览器都会默认开启混杂模式。但采用混杂模式不是什么值得推荐的做法,因为不同浏览器在这种模式下的行为差异非常大,如果不使用某些 hack 技术,跨浏览器的行为根本就没有一致性可言。

对于标准模式,可以通过使用 <!DOCTYPE html>(HTML5)来开启。而对于准标准模式,则可以通过使用过渡型(transitional)或框架集型(frameset)文档类型来触发。

准标准模式与标准模式非常接近,它们的差异几乎可以忽略不计。因此,当有人提到“标准模式”时,有可能是指这两种模式中的任何一种

noscript 元素

早期浏览器都面临一个特殊的问题,即当浏览器不支持 JavaScript 时如何让页面平稳地退化。最终解决方案是创造<noscript> 元素,用以在不支持 JavaScript 的浏览器中显示替代的内容。这个元素可以包含能够出现在文档 <body> 中的任何 HTML 元素——<script> 元素除外。包含在 <noscript> 元素中的内容只有在下列情况下才会显示出来:

  • 浏览器不支持脚本
  • 浏览器支持脚本,但脚本被禁用

符合上述任何一个条件,浏览器都会显示 <noscript> 中的内容。而在除此之外的其他情况下,浏览器不会呈现 <noscript> 中的内容

html
<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>

这个页面会在脚本无效的情况下向用户显示一条消息。而在启用了脚本的浏览器中,用户永远也不会看到它——尽管它是页面的一部分

现代用法示例:

html
<noscript>
  <div style="padding: 20px; background: #f0f0f0; border: 1px solid #ccc;">
    <h2>需要启用 JavaScript</h2>
    <p>此页面需要 JavaScript 才能正常工作。请启用浏览器的 JavaScript 功能。</p>
  </div>
</noscript>

JavaScript 生态系统

前端框架和库

现代 JavaScript 开发离不开各种框架和库,它们大大提高了开发效率:

主流前端框架:

  • React:Facebook 开发的 UI 库,采用组件化开发模式

  • Vue.js:渐进式框架,易于学习和使用

  • Angular:Google 开发的全功能框架,适合大型应用

    // ... 中间省略 ...

性能优化与最佳实践

代码优化原则

1. 减少 DOM 操作

code
// ❌ 避免:频繁的 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. 避免内存泄漏

javascript
// ❌ 常见内存泄漏:未清理的事件监听器
class Component {
  constructor() {
    this.handleClick = this.handleClick.bind(this)
    document.addEventListener("click", this.handleClick)
  }
  // ✅ 必须在组件销毁时清理
  destroy() {
    document.removeEventListener("click", this.handleClick)
  }
}

3. 使用防抖和节流

javascript
// 防抖(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
    }
  }
}

性能监控与调试

2. 使用 console 方法

code

### 代码质量与可维护性

**1. 使用严格模式**

```javascript
"use strict"
// 严格模式捕获常见错误、禁止 this 指向全局对象、更容易优化

2. 变量声明最佳实践

javascript
// ✅ 优先使用 const
const MAX_SIZE = 100
const config = { debug: true }

// ✅ 需要重新赋值时使用 let
let count = 0
count++

// ❌ 避免使用 var
var x = 1 // 函数作用域,容易导致问题

3. 函数设计原则

javascript
// ✅ 单一职责:一个函数只做一件事
function validateEmail(email) {
  const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
  return regex.test(email)
}

// ✅ 纯函数:相同输入总是产生相同输出
function add(a, b) {
  return a + b
}

4. 错误处理

javascript
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. 使用可选链和空值合并

javascript
const user = { profile: { name: "张三" } }
// ✅ 可选链操作符 ?.
const city = user?.profile?.address?.city // undefined(不会报错)
// ✅ 空值合并运算符 ??
const displayCity = city ?? "未知城市" // "未知城市"
// 与 || 的区别:空字符串用 ?? 不会被当作 falsy

2. 使用展开运算符

javascript
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. 模块化开发

javascript
// 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")

常见问题解答

1. JavaScript 基础问题

Q: var、let 和 const 有什么区别?

javascript
// var:函数作用域,存在变量提升
console.log(a) // undefined
var a = 1

// let:块级作用域,暂时性死区
// console.log(b) // ReferenceError
let b = 2

// const:块级作用域,必须初始化,不能重新赋值(但对象属性可修改)
const c = 3

Q: == 和 === 有什么区别?

javascript
// ==:宽松相等,会进行类型转换
1 == "1" // true
null == undefined // true

// ===:严格相等,不进行类型转换
1 === "1" // false
null === undefined // false
// ✅ 推荐:始终使用 ===

Q: 什么是闭包?

javascript
function createCounter() {
  let count = 0
  return {
    increment() { count++; return count },
    getCount() { return count }
  }
}
const counter = createCounter()
console.log(counter.increment()) // 1

2. 异步编程问题

Q: Promise、async/await 和回调函数的区别?

javascript
// 回调函数(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: 如何处理多个异步操作?

javascript
// 并行执行
const [users, products] = await Promise.all([fetchUsers(), fetchProducts()])
// 竞争执行
const result = await Promise.race([fetchFromServer1(), fetchFromServer2()])

3. 性能优化问题

Q: 如何优化 JavaScript 性能?

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: 什么是内存泄漏?如何避免?

javascript
// 1. 意外的全局变量
function leak() { bar = "I'm global now" } // ❌

// 2. 未清理的定时器
class Timer {
  constructor() { this.timer = setInterval(() => {}, 1000) }
  destroy() { clearInterval(this.timer) } // ✅ 必须清理
}

// 3. 闭包引用大对象 — 只保存需要的值

4. 调试技巧

Q: 如何高效调试 JavaScript?

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()

5. 兼容性与最佳实践

Q: 如何处理浏览器兼容性?

javascript
// 1. 特性检测
if ("IntersectionObserver" in window) {
  const observer = new IntersectionObserver(callback)
} else {
  window.addEventListener("scroll", throttle(callback, 100))
}
// 2. Polyfill + Babel 等转译工具将 ES6+ 转换为兼容代码

学习路径

基础阶段

  • 语法基础:变量、数据类型、运算符、控制结构、函数
  • DOM 操作:选择元素、修改内容和样式、事件处理
  • 异步编程:回调函数、Promise、async/await、事件循环

进阶阶段

  • ES6+ 特性:箭头函数、解构赋值、模板字符串、类、模块系统
  • 高级概念:闭包和作用域、this 绑定、原型和原型链、函数式编程
  • 工具和框架:选择一个主流框架深入学习,掌握构建工具和包管理器

高级阶段

  • 性能优化:代码分割和懒加载、内存管理、性能监控
  • 工程化:项目架构设计、代码组织和模块化、测试和部署
  • 全栈开发:Node.js 后端开发、数据库操作、API 设计

学习资源推荐

  • MDN Web Docs:最权威的 JavaScript 文档
  • JavaScript.info:详细的现代 JavaScript 教程
  • freeCodeCamp:免费的编程学习平台

学习建议

  1. 理论与实践结合:不仅要理解概念,还要多写代码
  2. 循序渐进:从基础开始,逐步深入
  3. 项目驱动:通过实际项目巩固知识
  4. 阅读源码:学习优秀开源项目的代码
  5. 保持更新:JavaScript 生态发展很快,需要持续学习

常见学习误区

  1. 跳过基础:直接学习框架,导致基础不牢
  2. 只看不练:只看教程不写代码
  3. 追求新技术:盲目追求最新技术,忽视基础
  4. 缺乏实践:没有实际项目经验

记住:扎实的基础是成为优秀 JavaScript 开发者的关键!