概念
-
在编译器工作流程中,词法分析是将源代码分解为一系列词法单元的过程。
-
词法单元包括标识符、关键字、运算符等。词法分析器会读取源代码的每一个字符,根据预定义的规则将它们组成一系列词法单元。
-
词法分析器通常使用有限状态机来实现。有限状态机是一种计算模型,它可以接受一串输入并根据一组状态转移规则进行状态转移,最终输出一个结果。在词法分析器中,有限状态机通常用于匹配输入的字符串。
-
词法分析器可以快速匹配输入的字符串,并且不需要回溯的操作,因此能够有效地提高编译器的词法分析效率。
-
词法分析器通常是编译器的第一个组件,它会将源代码分解为一系列词法单元,然后将这些词法单元传递给语法分析器进行语法分析。词法分析器的设计和实现对编译器的性能和正确性都有重要的影响。
例子:实现一个基本的词法分析器
需求:将<h1 id="title"><span>hello</span>world</h1>
转换成一系列词法单元。
最终效果:
- 定义类型
// tokenTypes.js
const Punctuator = "Punctuator"; // < > / 等一系列标识符
const JSXIdentifier = "JSXIdentifier"; // html标签
const AttributeKey = "AttributeKey"; // 标签属性的key(如:id, name等)
const AttributeStringValue = "AttributeStringValue"; // 标签属性的value(如:id="title"中的title)
const JSXText = "JSXText"; // 文本(如标签内部的文字)
const BackSlash = "BackSlash"; // 反斜杠(结束标签的/)
// 将类型导出
module.exports = {
Punctuator,
JSXIdentifier,
AttributeKey,
AttributeStringValue,
JSXText,
BackSlash,
};
- 具体实现
// tokenizer.js
// 导入类型
const {
Punctuator,
JSXIdentifier,
AttributeKey,
AttributeStringValue,
JSXText,
BackSlash,
} = require("./tokenTypes");
// 初始的token
let currentToken = { type: "", value: "" };
// 分词数组,最终生成的分词数据都会放在这里
let tokens = [];
// 定义正则,符合大写字母、小写字母、数字,则做什么事情
const LETTER = /[a-zA-Z0-9]/;
// 主函数
function tokenlizier(input) {
// 开始状态
let state = start;
debugger;
// 遍历收到的字符
for (let char of input) {
state = state(char);
}
return tokens;
}
// 首次执行时,匹配到 <
function start(char) {
if (char === "<") {
// 将类型及值发射出去,存到 tokens 中
emit({ type: Punctuator, value: "<" });
// 找到 < 之后继续收集
return foundLeftParentheses;
}
// 未找到 < 则抛出异常
throw new Error("第一个字符必须是<");
}
function foundLeftParentheses(char) {
// char: h1
if (LETTER.test(char)) {
currentToken.type = JSXIdentifier;
currentToken.value += char;
// 继续收集标识符
return jsxIdentifier;
} else if (char === "/") {
// 区分开始标签和结束标签
emit({ type: BackSlash, value: "/" });
return foundLeftParentheses;
}
throw TypeError("Error");
}
function jsxIdentifier(char) {
// char如果是字母或数字就接着增加
if (LETTER.test(char)) {
currentToken.value += char;
// 继续收集标识符
return jsxIdentifier;
} else if (char === " ") {
// 遇到了空格, 标识符结束,将当前收集的token发射出去
emit(currentToken);
// 开始收集属性
return attribute;
} else if (char === ">") {
// 开始标签结束了
emit(currentToken);
emit({ type: Punctuator, value: ">" });
// 找到>后继续收集
return foundRightParentheses;
}
throw TypeError("Error");
}
function attribute(char) {
// char=i
if (LETTER.test(char)) {
currentToken.type = AttributeKey;
currentToken.value += char;
// 继续收集属性
return attributeKey;
}
throw TypeError("Error");
}
function attributeKey(char) {
// char=d
if (LETTER.test(char)) {
currentToken.value += char;
// 继续收集
return attributeKey;
} else if (char === "=") {
emit(currentToken);
// 继续收集属性值
return attributeValue;
}
throw TypeError("Error");
}
function attributeValue(char) {
// cahr="
if (char === '"') {
currentToken.type = AttributeStringValue;
currentToken.value += char;
// 开始收集字符串属性值
return attributeStringValue;
}
throw TypeError("Error");
}
function attributeStringValue(char) {
// char=title
if (LETTER.test(char)) {
currentToken.value += char;
return attributeStringValue;
} else if (char === '"') {
emit(currentToken);
return tryLeaveAttribute;
}
throw TypeError("Error");
}
// 属性结束后,可能有新属性,也可以是 >
function tryLeaveAttribute(char) {
if (char === " ") {
// 如果是空格的话,说明后面还有一个新属性,再次开始收集属性
return attribute;
} else if (char === ">") {
// 如果是 >,则继续收集>
emit({ type: Punctuator, value: ">" });
// 找到 > 后继续收集
return foundRightParentheses;
}
throw TypeError("Error");
}
function foundRightParentheses(char) {
if (char === "<") {
emit({ type: Punctuator, value: "<" });
// 找到 < 之后继续收集
return foundLeftParentheses;
} else {
currentToken.type = JSXText;
currentToken.value += char;
return jsxText;
}
}
function jsxText(char) {
if (char === "<") {
emit(currentToken);
emit({ type: Punctuator, value: "<" });
return foundLeftParentheses;
} else {
currentToken.value += char;
return jsxText;
}
}
function emit(token) {
// 每次发射 token 后就将其加入到 tokens 数组中,并将 currentToken 清空
currentToken = { type: "", value: "" };
tokens.push(token);
}
// 测试用例
let sourceCode = `<h1 id="title"><span>hello</span>world</h1>`; // 要分词的 jsx
console.log(tokenlizier(sourceCode));
module.exports = {
tokenlizier,
};