深入理解 TypeScript 编译器(一):扫描器 Scanner —— 源码到 Token 流的转换器(TypeScript Deep Dive 中文版)
文档教程【免费下载链接】typescript-book-chineseTypeScript Deep Dive 中文版项目地址https://gitcode.com/gh_mirrors/ty/typescript-book-chinese点击查看免费下载本篇技术指南聚焦 TypeScript 编译器前端流水线的第一个环节——扫描器Scanner它负责把源代码SourceCode转化为Token 流供解析器Parser进一步生成抽象语法树AST。你将掌握createScanner单例扫描器的初始化方式initializeState、scan()的驱动循环、扫描器内部状态位置、Token 详情的读取 API以及如何脱离解析器独立使用扫描器扫描源码任意位置。文中代码均可直接运行复现适合希望从源码层面理解 TypeScript 编译器工作机理的开发者阅读。扫描器在编译器流水线中的位置在 TypeScript 编译器中源码到 JavaScript 输出的处理并不是一步到位的而是由若干相对独立的阶段串联而成。整个编译流水线的第一阶段就是扫描SourceCode源码 ~~ 扫描器 ~~ Token 流 ~~ 解析器 ~~ AST也就是说扫描器把一段字符串形式的源代码切分成一个个最小的语法单元——Token如关键字var、标识符foo、赋值号、数字字面量123、分号;之后解析器再依据这些 Token 序列构建抽象语法树。这两个阶段在 docs/compiler/overview.md 中均有定位说明Scanner 扫描器scanner.ts源码 → Token 流Parser 解析器parser.tsToken 流 → AST其后还有Binder 绑定器AST → Symbols、Checker 检查器AST Symbols → 类型验证与Emitter 发射器AST Checker → JavaScript对应文档见 docs/compiler/binder.md、docs/compiler/checker.md、docs/compiler/emitter.md。按照 docs/compiler/scanner.md 的说明TypeScript 扫描器的源码均位于编译器源码的scanner.ts中。在内部扫描器不是独立驱动的而是由解析器控制解析器不断调用扫描器的扫描函数获取下一个 Token并根据当前 Token 的种类决定如何继续解析。这也是理解本篇文章全部内容的前提——扫描器是解析器的生产工具解析器是扫描器的消费者。注本仓库typescript-book-chinese是《TypeScript Deep Dive》的中文翻译版为纯文档型仓库。原文档所描述的scanner.ts、parser.ts等编译器源码位于 TypeScript 官方仓库的src/compiler目录下且如 docs/compiler/overview.md 中的译注所述书籍对应的源码为 2016 年以前的版本学习时建议对照现行源码阅读。解析器对扫描器的使用单例 状态重置为避免重复创建扫描器带来的开销parser.ts中维护了一个扫描器的单例。解析器在使用它之前会根据需要调用initializeState函数对该扫描器进行准备即重置其文本内容、错误回调、脚本目标与语言变体等状态。我们可以用一个与initializeState相似的函数来演示这一概念。下面这段代码取自原书的可运行示例code/compiler/scanner/runScanner.ts它创建了一个单例扫描器初始化后持续调用scan()直到遇到EndOfFileToken并打印每个 Token 的语法类别名称import * as ts from ntypescript; // 单例扫描器 const scanner ts.createScanner(ts.ScriptTarget.Latest, /* 忽略杂项 */ true); // 此函数与初始化使用的 initializeState 函数相似 function initializeState(text: string) { scanner.setText(text); scanner.setOnError((message: ts.DiagnosticMessage, length: number) { console.error(message); }); scanner.setScriptTarget(ts.ScriptTarget.ES5); scanner.setLanguageVariant(ts.LanguageVariant.Standard); } // 使用示例 initializeState( var foo 123; .trim() ); // 开始扫描 var token scanner.scan(); while (token ! ts.SyntaxKind.EndOfFileToken) { console.log(ts.formatSyntaxKind(token)); token scanner.scan(); }关于这段示例有几个值得注意的细节ts.createScanner(ts.ScriptTarget.Latest, true)第一个参数指定脚本目标这里用Latest第二个参数为true表示跳过/忽略杂项Trivia即空白、注释等对语法分析不重要的内容这正是编译器在正式解析时所采用的行为。setText(text)把待扫描的源码字符串交给扫描器这是任何扫描操作前必须完成的一步。setOnError注册错误回调扫描过程中遇到非法字符或无法识别的 Token 时会触发。setScriptTarget与setLanguageVariant设置脚本目标示例中改为ES5与语言变体示例中用Standard即非 JSX 的标准变体二者会影响扫描器对某些 Token 的识别规则。scanner.scan()每次调用前进一个 Token返回该 Token 的SyntaxKind枚举值当扫描到文件末尾时返回ts.SyntaxKind.EndOfFileToken因此示例用while循环驱动扫描直到文件结束。ts.formatSyntaxKind(token)把SyntaxKind枚举值格式化为人类可读的字符串如VarKeyword方便调试与教学演示。该段代码对输入var foo 123;会输出以下内容VarKeyword Identifier FirstAssignment FirstLiteralToken SemicolonToken可以看到一行简单的var foo 123;被扫描器拆成了 5 个 Token关键字varVarKeyword、标识符fooIdentifier、赋值运算符FirstAssignment即第一类赋值 Token 的统称、数字字面量123FirstLiteralToken即第一类字面量 Token 的统称以及语句结束的分号SemicolonToken。扫描器状态位置信息与 Token 详情调用scan()之后扫描器会更新其内部状态主要包括当前扫描位置和当前 Token 详情Token 类型、起始位置、文本等。扫描器暴露了一组工具函数用于读取这些状态其中最重要的是API作用getStartPos()返回扫描器当前或上一个 Token的起始字符位置getToken()返回当前 Token 的SyntaxKindgetTokenText()返回当前 Token 对应的源文本getTokenPos()/getTextPos()读取 Token 起始位置与当前文本扫描位置getText()返回当前正在扫描的完整源码文本下面的示例原书code/compiler/scanner/runScannerWithPosition.ts演示了如何用getStartPos()在扫描过程中记录每个 Token 的起止字符偏移// 使用示例 initializeState( var foo 123; .trim() ); // 开始扫描 var token scanner.scan(); while (token ! ts.SyntaxKind.EndOfFileToken) { let currentToken ts.formatSyntaxKind(token); let tokenStart scanner.getStartPos(); token scanner.scan(); let tokenEnd scanner.getStartPos(); console.log(currentToken, tokenStart, tokenEnd); }注意这里的技巧getStartPos()返回的是下一次扫描起始的位置因此要获得当前 Token 的结束位置需要先调用scan()前进到下一个 Token再读取getStartPos()。该代码输出如下VarKeyword 0 3 Identifier 3 7 FirstAssignment 7 9 FirstLiteralToken 9 13 SemicolonToken 13 14对照源码文本var foo 123;14 个字符下标 0~13可以清晰地验证 Token 切分的边界Token起始结束对应文本VarKeyword03varIdentifier37fooFirstAssignment79FirstLiteralToken913123SemicolonToken1314;每个 Token 的结束位置恰好是下一个 Token 的起始位置Token 之间无缝衔接——这正是扫描器按顺序推进这一基本性质的直观体现。这类精确的字符偏移在编译器的后续阶段非常重要AST 节点正是基于 Token 的起止位置来记录自身的pos/end参见 docs/compiler/ast.md 中对TextRange的描述错误报告也需要定位到具体的行、列与字符位置。扫描器状态的来源initializeState 与解析器的联动在真实编译流程中扫描器的初始化并非由使用者手工调用上述initializeState函数而是由解析器在parseSourceFile阶段完成。从 docs/compiler/parser.md 可以梳理出如下调用链程序(Program) - CompilerHost.getSourceFile - (全局函数 parser.ts) createSourceFile - Parser.parseSourceFile其中parseSourceFile不仅准备好解析器的状态还会调用initializeState准备好扫描器的状态随后通过parseSourceFileWorker进入正式的解析循环。解析器内部实现为namespace Parser持有各种状态变量以及单例扫描器const scanner。更进一步解析器中最重要的parseStatements函数会根据扫描器返回的当前 Token来决定调用哪个parseXXX函数例如当前 Token 是SemicolonToken就调用parseEmptyStatement生成空语句节点如果是VarKeyword则进入变量声明解析。也就是说扫描器输出的 Token 流直接驱动了解析器的语法分支选择——这也是为什么单例扫描器与解析器共享、且解析的每一步都向前看一个 Token 的原因。当解析器需要创建节点时createNode会使用当前扫描器状态提供的位置信息作为节点的初始位置见 docs/compiler/parser.md 中对createNode(kind, pos?)的说明而parseExpected在做 Token 匹配校验时也依赖扫描位置调用parseErrorAtPosition生成精准的错误报告。因此可以说扫描器的状态维护质量直接决定了 AST 节点位置的正确性与诊断信息的准确性。独立扫描器脱离解析器自由使用虽然 TypeScript 解析器内部使用的是单例扫描器但createScanner本身是公开 API你可以随时创建独立的扫描器然后通过setText/setTextPos随意扫描文件的不同位置import * as ts from ntypescript; const scanner ts.createScanner(ts.ScriptTarget.ESNext, true); scanner.setText(const a 1;\nconst b 2;); // 跳转到任意位置后开始扫描 scanner.setTextPos(14); // 跳到第二行的起始处 let token scanner.scan(); while (token ! ts.SyntaxKind.EndOfFileToken) { console.log(ts.formatSyntaxKind(token)); token scanner.scan(); }独立扫描器的主要用途包括自定义词法分析工具不依赖完整编译流程仅对某段文本做 Token 化处理例如编写代码高亮器、统计工具或代码片段分析脚本增量 / 定向扫描通过setTextPos直接从源码的任意偏移处开始扫描无需从头解析整个文件适合编辑器插件这类需要局部读取的场景教学与调试正如本篇文章开头两个示例所示独立扫描器是理解 Token 切分、位置信息与SyntaxKind枚举的最直接实验工具。扫描器与 AST 的衔接Token 位置与杂项Trivia扫描器输出的 Token 流虽然不会原样保存在 AST 中但它定义了两个与 AST 节点位置密切相关的概念在 docs/compiler/ast.md 中有详细说明Token Start一个 Token 的文本实际开始的位置自然的、直观的位置Full Start扫描器从上一个重要 Token开始扫描的位置即包含前导杂项空白、注释等在内的完整起始位置。AST 节点通过getStart()与getFullStart()分别获取这两种位置。例如对源码片段debugger;/*hello*/ //bye /*hi*/ functionfunction的 token start 是function关键字本身的位置而 full start 则要追溯到/*hello*/注释的位置——因为扫描器在遇到下一个重要 Token 之前会把这些杂项一并扫过。这一区别的根源正是扫描器对杂项的处理策略一个 Token 拥有它后面同一行到下一个 Token 之前的所有杂项该行之后的注释归属于下一个 Token源文件的第一个 Token 拥有所有起始杂项文件末尾剩余的杂项附加到长度为零的文件结束符EndOfFileToken上。也正因如此编译器的正式解析会令扫描器跳过杂项createScanner的第二个参数传true保持 AST 的轻量需要注释等杂项信息时再通过ts.getLeadingCommentRanges、ts.getTrailingCommentRanges等 API 按需获取。小结与学习建议本文基于 docs/compiler/scanner.md 及其在编译流水线中的上下文梳理了 TypeScript 扫描器的核心机制角色定位扫描器把源码转换为 Token 流是解析器生成 AST 的前置阶段SourceCode → Token 流 → AST单例模式解析器内部持有单例扫描器通过initializeStatesetText/setOnError/setScriptTarget/setLanguageVariant准备状态避免重复创建的开销驱动方式scan()每次返回一个SyntaxKind循环直至EndOfFileTokenformatSyntaxKind用于输出可读 Token 名称状态读取getStartPos()等 API 提供 Token 的精确字符位置这些位置信息会流转到 AST 节点pos/end与错误诊断中独立使用createScannersetText/setTextPos支持脱离解析器做任意位置的词法扫描。如需进一步深入建议按以下顺序阅读本仓库的编译器系列文档docs/compiler/overview.md整体架构→ docs/compiler/scanner.md本篇→ docs/compiler/parser.md解析器如何消费 Token 流→ docs/compiler/ast.mdToken 位置如何映射为节点位置→ docs/compiler/binder.md符号绑定。另外需注意本仓库对应的是 2016 年以前的 TypeScript 源码现行版本的scanner.ts在 API 细节上已有所演进建议对照官方现行源码阅读验证。赞分享文档教程【免费下载链接】typescript-book-chineseTypeScript Deep Dive 中文版项目地址https://gitcode.com/gh_mirrors/ty/typescript-book-chinese点击查看免费下载相关推荐TypeScript 编译器 Scanner 深度解析从源码到 Token 流的词法分析之旅TypeScript 编译器 Scanner 深度解析从源码到 Token 流的词法分析之旅 本篇技术指南以 docs/compiler/scanner.md教程TypeScript 编译器中的 Program编译上下文的统一入口typescript-book 编译器解析TypeScript 编译器中的 Program编译上下文的统一入口typescript book 编译器解析 TypeScript 编译器的执行起点是一教程TypeScript Deep Dive中文版从JavaScript到TypeScript的完美迁移指南TypeScript Deep Dive中文版从JavaScript到TypeScript的完美迁移指南 想要从JavaScript平滑过渡到TypeScri文档教程上一篇如何用QuPath实现病理图像快速分析新手完整免费指南下一篇yuzu模拟器完全指南在电脑上免费畅玩Switch游戏的终极教程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻

Zeek SumStats 框架详解:用观察流、Reducer 与 Epoch 构建可伸缩的网络统计与阈值检测

Zeek SumStats 框架详解:用观察流、Reducer 与 Epoch 构建可伸缩的网络统计与阈值检测

网络安全网络IDS 【免费下载链接】zeek Zeek is a powerful network analysis framework that is much different from the typical IDS you may know. 项目地址: https://gitcode.com/gh_mirrors/ze/zeek 点击查看 免费下载 导读 本文以 Zeek 的 Summary Statist…

2026/11/8 19:19:01 阅读更多 →
Linux - VMware搭建CentOS环境

Linux - VMware搭建CentOS环境

文章目录1 VMware安装2 CentOS7安装2.1 虚拟机配置2.1 虚拟机安装2.2 虚拟机快照及克隆2.3 自动获取IP2.4 设置固定IP2.5 工具安装1 VMware安装 本次部署如下,版本:VMware-workstation-full-17.5.0-22583795.exe,参考教程VMware安装教程 …

2026/10/30 18:21:04 阅读更多 →
游戏引擎对象与资源管理:解耦设计与工业级实践

游戏引擎对象与资源管理:解耦设计与工业级实践

1. 项目概述:为什么“游戏对象与资源管理”是引擎架构的生死线你打开一个现代3A游戏,画面里有上百个NPC在街巷间穿行,每人都有独立的AI行为树、骨骼动画、材质反射和物理碰撞体;远处教堂尖顶的彩绘玻璃在阳光下折射出动态光斑&…

2026/11/12 5:31:09 阅读更多 →

最新新闻

Electron桌面应用开发实战:从环境搭建到打包发布全流程

Electron桌面应用开发实战:从环境搭建到打包发布全流程

做前端这些年,我大部分时间都在跟浏览器标签页打交道,直到第一次把桌面应用跑起来,才发现原来自己一直被限制在地址栏和刷新按钮围起来的框框里。Electron这个词,最开始是在某段时间频繁出现在同事的讨论里,一个用HTML…

2026/11/12 5:00:00 阅读更多 →
滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

简介:亚太天能T109滑盖智能锁产品说明书是一份PDF格式的官方技术文档,主要面向智能锁安装人员、终端用户及售后维护者,帮助快速掌握产品安装、配置与常见故障处理。全包仅1个PDF文件,大小4.37MB,内容结构清晰&#xff…

2026/11/12 5:13:20 阅读更多 →
PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

上个月我在调试一台高速AGV的驱动系统,整车要求空载跑到8m/s,电机转速已经拉到额定点,驱动器的输出电压也顶到了母线电压极限附近。再往上踩速度指令,转速纹丝不动,三相电流反而开始震荡。那会儿我的第一反应就是&…

2026/11/12 5:13:20 阅读更多 →
C++核心知识点:从内存管理到STL的实战指南

C++核心知识点:从内存管理到STL的实战指南

C 这门语言我前前后后折腾了十几年,用它写过嵌入式驱动、做过游戏服务端,也搭过基础组件库。每次看到新人一头扎进"C 核心知识点"这个搜索词,我其实挺感慨的——这门语言的难点从来不是语法多,而是知识点之间的耦合太深…

2026/11/11 17:16:37 阅读更多 →
T109滑盖智能锁怎么装?安装调试与故障排查全攻略

T109滑盖智能锁怎么装?安装调试与故障排查全攻略

简介:亚太天能T109滑盖智能锁产品说明书以PDF格式发布,适合智能锁用户、安装人员与售后维修人员阅读。内容围绕T109系列指纹密码刷卡锁展开,先介绍前面板、后面板、锁体、机械钥匙、电池盒盖等部件名称与功能,再给出完整安装步骤&…

2026/11/11 17:16:37 阅读更多 →
LLM知识降熵:从信息垄断到理解权下放

LLM知识降熵:从信息垄断到理解权下放

你有没有过这种经历:翻开源码注释、论文摘要或者一份专业报告,每个字都认识,连在一起却像天书。想找人聊聊,身边要么没人懂,要么懂的懒得跟你解释。后来我试着把同一段内容丢给一个大语言模型,几秒钟之后&a…

2026/11/12 5:13:20 阅读更多 →

日新闻

周新闻

月新闻