深入理解 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/10/12 10:06:04 阅读更多 →
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/13 5:11:37 阅读更多 →
游戏引擎对象与资源管理:解耦设计与工业级实践

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

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

2026/10/12 2:50:32 阅读更多 →

最新新闻

学术AI工具怎么选更划算?六款科研工具按环节搭配的性价比盘点

学术AI工具怎么选更划算?六款科研工具按环节搭配的性价比盘点

一、工具越多,性价比这笔账越算越乱1、行业背景。 近几年大模型能力快速落地,学术场景成了试验田。检索、翻译、改写、润色、绘图,几乎每个环节都冒出专门工具,订阅方式从免费额度到按年付费各有不同。选择变多了,判断成本也跟着涨,同一笔预算怎么分配成了新问题。2、不同人群的…

2026/10/14 17:01:19 阅读更多 →
仓颉语言服务 LSPServer 使用指南:启动参数、运行环境与源码级原理解析

仓颉语言服务 LSPServer 使用指南:启动参数、运行环境与源码级原理解析

开发工具CLI代码质量 【免费下载链接】cangjie_tools 仓颉编程语言命令行工具,包括仓颉包管理工具、仓颉格式化工具、仓颉多语言桥接工具及仓颉语言服务。 项目地址: https://gitcode.com/Cangjie/cangjie_tools 点击查看 免费下载 本文以 cangjie-lang…

2026/10/14 17:01:19 阅读更多 →
openJiuwen MemoryCrypto 解析:记忆引擎 AES-256-GCM 加解密实战指南

openJiuwen MemoryCrypto 解析:记忆引擎 AES-256-GCM 加解密实战指南

人工智能AI AgentAgent 框架工具调用Agent 工作流RAGMCP Clients强化学习 【免费下载链接】agent-core-java openJiuwen agent core Java是 openJiuwen Core的 Java版本,提供AI Agent开发、运行、调优与演进相关的全套SDK能力。 项目地址: https://gitco…

2026/10/14 17:01:19 阅读更多 →
树莓派还是Arduino?跑系统还是跑裸机?从真实项目谈选型

树莓派还是Arduino?跑系统还是跑裸机?从真实项目谈选型

很多刚开始搞硬件开发的朋友,总会拿着一块树莓派和一块 Arduino 问我:“到底买哪个?”几乎所有社区里都会有人甩出一句经典老话:“一个跑系统,一个跑裸机。”听起来很合理,选型好像一下子就清楚了。但这几年…

2026/10/14 17:01:19 阅读更多 →
手动搭建MySQL MHA高可用架构:从主库宕机到自动切换实战

手动搭建MySQL MHA高可用架构:从主库宕机到自动切换实战

1. 为什么手动搭建MHA依然值得学1.1 从一次主库宕机说起凌晨两点,监控告警响了,主库连接数直接掉零。当时我们那套业务用的是单点MySQL,没有做任何高可用。运维同学从被窝里爬起来,手动切从库、改配置、重启服务,前后折…

2026/10/14 17:01:19 阅读更多 →
Oracle 19c RAC on Linux 7.6 安装实战:从环境检查到静默建库

Oracle 19c RAC on Linux 7.6 安装实战:从环境检查到静默建库

简介:面向需要在Red Hat Enterprise Linux 7.6上部署Oracle 19c RAC的DBA与运维工程师,这份安装手册将环境准备与集群配置拆分为可逐步跟做的阶段,涵盖OS环境检查、关闭THP并开启Hugepages、依赖包与内核参数设置、时钟同步等前置任务&#x…

2026/10/14 17:00:18 阅读更多 →

日新闻

AnyPS5通用适配层设计:兼容性与性能的工程取舍

AnyPS5通用适配层设计:兼容性与性能的工程取舍

1. 从"AnyPS5"这个名字说起:它到底想解决什么问题第一次看到"AnyPS5"这个标题,我脑子里冒出来的第一个念头是:这大概率是一个围绕"跨平台运行"或者"通用适配"做文章的项目。名字里的"Any"…

2026/10/14 0:00:41 阅读更多 →
Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

1. 项目概述:Flutter跨平台二手交易应用开发作为一名长期从事移动应用开发的工程师,我最近完成了一个基于Flutter框架的二手物品交易平台项目。这个项目最吸引我的地方在于它完美结合了Flutter的跨平台优势与HarmonyOS的生态特性,实现了"…

2026/10/14 0:00:41 阅读更多 →
DFS算法解决飞机降落调度问题实战

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:41 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/14 16:30:29 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 22:01:27 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 7:53:34 阅读更多 →