この懸念をクリックし、公開番号、最新の文書の更新を取得するために、このガイドでサポートを受けることができ、「フロントエンドの面接マニュアル」だけでなく、ほとんどの標準的な履歴書のテンプレートを。
序文
バベルは、近代的なJavaScriptのシンタックスコンバータで、ほぼすべての近代的なフロントエンドのプロジェクトで彼の影を見ることができ、ほとんどの開発者のための原理はまた、ブラックボックスに属しているが、背景にある原理を理解するためのツールとして、バベルは本当に必要それは?
唯一バベルでし本当に必要でない場合は、問題を含むがこれらに限定されない、あまりにも原則として用途の広い範囲の我々の開発の背後にある:eslint jshint stylelint CSS-で-jsからきれいJSXのVUE-テンプレートuglify-jsからpostcss少ない、など、など、検出するためのコードテンプレートから、混乱から圧縮トランスコーディングに、エディタを強調してもコードが密接に彼らと一緒にリンクされています。
あなたが興味を持っている場合、あなたは、いくつかの黒魔術を得ることができます:フロントエンドエンジニアは、コンパイラ理論で何ができますか?
フロント
大まかに3つの部分に分かれバベル:
- 分析:AST(抽象構文木)にコード(実際には、文字列)に変換
- 変換:ASTのアクセスノードが新しいASTを生成するための変換操作を行い、
- 生成:新しいASTベースのコード生成
我々は追加の検証を大幅に節約ので、私たちは主バベルの基本的な原理を理解するために、ミニバベルを作成することによって、このミニチュアのバベルは、単一の機能も非常に悲しいですが、コードの400行が残っている、バベルのその実装の詳細は、同じではありません情報分析だけ近代的な準拠のJavaScriptシンタックスパーサは、コードの5000行が必要になりますので、すぐにバベルの基本的な実現を理解するために私たちを助けていないので、このミニバベルは、それが(理由は何も便利を示さないに加えて)やや無味ですが、比較的完全まだ興味があれば、あなたが読むことができる、エントリー後のエントリとしてこれを使用することができバベルの基本原理を示しています。
コード分析
パーサのコンセプト
コード分析、つまり、私たちはしばしばパーサは、コードの一部(テキスト)のためのデータ構造に解析されたと言います。
このコードES6例えば
const add = (a, b) => a + b
私たちはバベルこのフォームの後に解決されます。
{
"type": "File",
"start": 0,
"end": 27,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 1,
"column": 27
}
},
"program": {
"type": "Program",
"start": 0,
"end": 27,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 1,
"column": 27
}
},
"sourceType": "module",
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 27,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 1,
"column": 27
}
},
"declarations": [
{
"type": "VariableDeclarator",
"start": 6,
"end": 27,
"loc": {
"start": {
"line": 1,
"column": 6
},
"end": {
"line": 1,
"column": 27
}
},
"id": {
"type": "Identifier",
"start": 6,
"end": 9,
"loc": {
"start": {
"line": 1,
"column": 6
},
"end": {
"line": 1,
"column": 9
},
"identifierName": "add"
},
"name": "add"
},
"init": {
"type": "ArrowFunctionExpression",
"start": 12,
"end": 27,
"loc": {
"start": {
"line": 1,
"column": 12
},
"end": {
"line": 1,
"column": 27
}
},
"id": null,
"generator": false,
"expression": true,
"async": false,
"params": [
{
"type": "Identifier",
"start": 13,
"end": 14,
"loc": {
"start": {
"line": 1,
"column": 13
},
"end": {
"line": 1,
"column": 14
},
"identifierName": "a"
},
"name": "a"
},
{
"type": "Identifier",
"start": 16,
"end": 17,
"loc": {
"start": {
"line": 1,
"column": 16
},
"end": {
"line": 1,
"column": 17
},
"identifierName": "b"
},
"name": "b"
}
],
"body": {
"type": "BinaryExpression",
"start": 22,
"end": 27,
"loc": {
"start": {
"line": 1,
"column": 22
},
"end": {
"line": 1,
"column": 27
}
},
"left": {
"type": "Identifier",
"start": 22,
"end": 23,
"loc": {
"start": {
"line": 1,
"column": 22
},
"end": {
"line": 1,
"column": 23
},
"identifierName": "a"
},
"name": "a"
},
"operator": "+",
"right": {
"type": "Identifier",
"start": 26,
"end": 27,
"loc": {
"start": {
"line": 1,
"column": 26
},
"end": {
"line": 1,
"column": 27
},
"identifierName": "b"
},
"name": "b"
}
}
}
}
],
"kind": "const"
}
],
"directives": []
}
}
私たちは、単純なパーサを書くために、目標として上記の機能を解決するために矢印ES6。
テキスト--->プロセス内のAST二つの重要なステップがあります:
- 字句解析:コード(文字列)は、トークンストリーム内に、すなわち分割されたシンタックス要素を配列に
- 構文解析トークンストリーム(配列は、上記生成された)及びASTを生成します
字句解析(トークナイザ - 字句解析)
字句解析を行い、我々は最初に所属したJavaScriptに理解する必要がある構文ユニット
- デジタル:科学表記法ではJavaScriptと構文は、すべての通常のアレイユニットです。
- 括弧「(」、「)」限り存在にかかわらず、任意の有意と考えられている文法的ユニット
- 識別子:連続文字、共通変数、定数(例:ヌル真)、キーワード(ブレークする場合)など
- 演算子:+、 - 、*、/、など
- もちろんなど括弧内のコメントがあります
私たちのパーサの過程で、それは別の角度コードで表示する必要があります、我々は通常のコードで動作する基本的に文字列またはテキストの一部である、それは意味がありません、JavaScriptエンジンは、それが意味を与えますので、私たちは、構文解析をコーディングしていますただ文字列。
例として、依然として次のコード
const add = (a, b) => a + b
私たちは、このような結果を期待します
[
{ type: "identifier", value: "const" },
{ type: "whitespace", value: " " },
...
]
だから我々は今、トークナイザ(字句解析器)を構築するために始めます
// 词法分析器,接收字符串返回token数组
export const tokenizer = (code) => {
// 储存 token 的数组
const tokens = [];
// 指针
let current = 0;
while (current < code.length) {
// 获取指针指向的字符
const char = code[current];
// 我们先处理单字符的语法单元 类似于`;` `(` `)`等等这种
if (char === '(' || char === ')') {
tokens.push({
type: 'parens',
value: char,
});
current ++;
continue;
}
// 我们接着处理标识符,标识符一般为以字母、_、$开头的连续字符
if (/[a-zA-Z\$\_]/.test(char)) {
let value = '';
value += char;
current ++;
// 如果是连续字那么将其拼接在一起,随后指针后移
while (/[a-zA-Z0-9\$\_]/.test(code[current]) && current < code.length) {
value += code[current];
current ++;
}
tokens.push({
type: 'identifier',
value,
});
continue;
}
// 处理空白字符
if (/\s/.test(char)) {
let value = '';
value += char;
current ++;
//道理同上
while (/\s]/.test(code[current]) && current < code.length) {
value += code[current];
current ++;
}
tokens.push({
type: 'whitespace',
value,
});
continue;
}
// 处理逗号分隔符
if (/,/.test(char)) {
tokens.push({
type: ',',
value: ',',
});
current ++;
continue;
}
// 处理运算符
if (/=|\+|>/.test(char)) {
let value = '';
value += char;
current ++;
while (/=|\+|>/.test(code[current])) {
value += code[current];
current ++;
}
// 当 = 后面有 > 时为箭头函数而非运算符
if (value === '=>') {
tokens.push({
type: 'ArrowFunctionExpression',
value,
});
continue;
}
tokens.push({
type: 'operator',
value,
});
continue;
}
// 如果碰到我们词法分析器以外的字符,则报错
throw new TypeError('I dont know what this character is: ' + char);
}
return tokens;
};
この機能のためだけES6は、その余分な仕事がないので、だから、私たちの基本的な字句解析器は、完全に作成するために(余分な負荷が非常に大きくなります)。
const result = tokenizer('const add = (a, b) => a + b')
console.log(result);
/**
[ { type: 'identifier', value: 'const' },
{ type: 'whitespace', value: ' ' },
{ type: 'identifier', value: 'add' },
{ type: 'whitespace', value: ' ' },
{ type: 'operator', value: '=' },
{ type: 'whitespace', value: ' ' },
{ type: 'parens', value: '(' },
{ type: 'identifier', value: 'a' },
{ type: ',', value: ',' },
{ type: 'whitespace', value: ' ' },
{ type: 'identifier', value: 'b' },
{ type: 'parens', value: ')' },
{ type: 'whitespace', value: ' ' },
{ type: 'ArrowFunctionExpression', value: '=>' },
{ type: 'whitespace', value: ' ' },
{ type: 'identifier', value: 'a' },
{ type: 'whitespace', value: ' ' },
{ type: 'operator', value: '+' },
{ type: 'whitespace', value: ' ' },
{ type: 'identifier', value: 'b' } ]
**/
1.3解析
字句解析、構文解析は、我々は次のであるため、はるかに複雑なものよりも、コードの概略ので、それがマイクロバベルコードの全体量に任されていても、コードを省略するために、「任意」の決定の多くを行います。
構文解析のための様々な構文解析するための可能性によって複雑にされ、開発者は、(我々がトークン上のアレイを生成する)情報トークンストリームに基づいて、コード間の論理的な関係を分析する必要がある唯一の字句解析後に提供構造化された抽象構文木になるためにトークンストリーム。
最高の基準に従って構文解析を行い、ほとんどのJavaScriptパーサーが続くestree仕様
標準コンテンツの多くは、読み興味がある可能性がありますので、我々はより多くの重要な基準のいくつかをご紹介します:
ステートメント(ステートメント):判決、例外は声明で、文を扱う場合ステートメントは、JavaScriptのシンタックス、私たちの共通のサイクルでは非常に一般的であり、そのすべての文があります
// 典型的for 循环语句
for (var i = 0; i < 7; i++) {
console.log(i);
}
式(式):式はコードの集まりである、それは値を返し、式が別の非常に一般的な文法で使用すると、式が何であるかを理解していない場合は、関数式は、一般的な表現であり、MDNは、非常に詳細な説明があります。
// 函数表达式
var add = function(a, b) {
return a + b
}
ステートメント(宣言):、分割変数宣言や関数宣言を宣言し、この文で使用される関数式式(式)の例は、以下の文言です。
// 函数声明
function add(a, b) {
return a + b
}
あなたは少し混乱することができ、それらの間の関係を明確にするために、我々は解釈するには、次のコード例を持っています
// 函数表达式
var add = function(a, b) {
return a + b
}
まず、このコードの全体の本質は、変数宣言(VariableDeclarator)です。
変数は、関数式(FunctionExpression)として宣言されています。
大規模な声明(BlockStatement)のためのかっこを含む関数式、:
バルク部分でreturn文は、return文(ReturnStatement)です。
戻り値は、実際に二項演算子またはバイナリ表現(BinaryExpressionを)と呼ばれています。
これらのいくつかは、彼らが我々はAST(抽象構文木)と呼ばれて言及していない解析された後、さらに多くありますが、いくつかは、いくつかのもあり、もちろん、ステートメントを宣言している、上記の式に属します。
アイデアは、最終的にはトークンの分析に類似しているとき、我々はそれがwhileループ(WhileStatement)の一部である場合はそのループ、文が巨大である声明(BlockStatement)またはループであれば、レベルの発現またはステートメントどの属するパースありませんまたは必然的にさえスコープの問題を検討しなければならないなどのループ(ForStatement)、のために、非常に複雑な構文解析もこの中に反映されています。
const parser = tokens => {
// 声明一个全时指针,它会一直存在
let current = -1;
// 声明一个暂存栈,用于存放临时指针
const tem = [];
// 指针指向的当前token
let token = tokens[current];
const parseDeclarations = () => {
// 暂存当前指针
setTem();
// 指针后移
next();
// 如果字符为'const'可见是一个声明
if (token.type === 'identifier' && token.value === 'const') {
const declarations = {
type: 'VariableDeclaration',
kind: token.value
};
next();
// const 后面要跟变量的,如果不是则报错
if (token.type !== 'identifier') {
throw new Error('Expected Variable after const');
}
// 我们获取到了变量名称
declarations.identifierName = token.value;
next();
// 如果跟着 '=' 那么后面应该是个表达式或者常量之类的,额外判断的代码就忽略了,直接解析函数表达式
if (token.type === 'operator' && token.value === '=') {
declarations.init = parseFunctionExpression();
}
return declarations;
}
};
const parseFunctionExpression = () => {
next();
let init;
// 如果 '=' 后面跟着括号或者字符那基本判断是一个表达式
if (
(token.type === 'parens' && token.value === '(') ||
token.type === 'identifier'
) {
setTem();
next();
while (token.type === 'identifier' || token.type === ',') {
next();
}
// 如果括号后跟着箭头,那么判断是箭头函数表达式
if (token.type === 'parens' && token.value === ')') {
next();
if (token.type === 'ArrowFunctionExpression') {
init = {
type: 'ArrowFunctionExpression',
params: [],
body: {}
};
backTem();
// 解析箭头函数的参数
init.params = parseParams();
// 解析箭头函数的函数主体
init.body = parseExpression();
} else {
backTem();
}
}
}
return init;
};
const parseParams = () => {
const params = [];
if (token.type === 'parens' && token.value === '(') {
next();
while (token.type !== 'parens' && token.value !== ')') {
if (token.type === 'identifier') {
params.push({
type: token.type,
identifierName: token.value
});
}
next();
}
}
return params;
};
const parseExpression = () => {
next();
let body;
while (token.type === 'ArrowFunctionExpression') {
next();
}
// 如果以(开头或者变量开头说明不是 BlockStatement,我们以二元表达式来解析
if (token.type === 'identifier') {
body = {
type: 'BinaryExpression',
left: {
type: 'identifier',
identifierName: token.value
},
operator: '',
right: {
type: '',
identifierName: ''
}
};
next();
if (token.type === 'operator') {
body.operator = token.value;
}
next();
if (token.type === 'identifier') {
body.right = {
type: 'identifier',
identifierName: token.value
};
}
}
return body;
};
// 指针后移的函数
const next = () => {
do {
++current;
token = tokens[current]
? tokens[current]
: { type: 'eof', value: '' };
} while (token.type === 'whitespace');
};
// 指针暂存的函数
const setTem = () => {
tem.push(current);
};
// 指针回退的函数
const backTem = () => {
current = tem.pop();
token = tokens[current];
};
const ast = {
type: 'Program',
body: []
};
while (current < tokens.length) {
const statement = parseDeclarations();
if (!statement) {
break;
}
ast.body.push(statement);
}
return ast;
};
これまでのところ、我々が持っている暴力のパーサにトークンストリームを、最終的にはシンプルな抽象構文木を得ました:
{
"type": "Program",
"body": [
{
"type": "VariableDeclaration",
"identifierName": "add",
"init": {
"type": "ArrowFunctionExpression",
"params": [
{
"type": "identifier",
"identifierName": "a"
},
{
"type": "identifier",
"identifierName": "b"
}
],
"body": {
"type": "BinaryExpression",
"left": {
"type": "identifier",
"identifierName": "a"
},
"operator": "+",
"right": {
"type": "identifier",
"identifierName": "b"
}
}
}
}
]
}
トランスコーディング
どのようにコードを変換するには?
私たちは、ユーザーが最も一般的バベル、私たちが生まれたカスタムコードの変換規則を使用バベルプラグインでトランスコーディングに使用されて置き、最初のもののコードの解析と生成バベルの主な原因です。
例えば、我々はバベルを使っ行う小さなプログラムコンバータをオンに反応して、大まかな状況バベルのワークフローはこれです:
- バベルは、抽象構文木のコード解析を反応します
- プラグバベルを使用して、開発者は、元の抽象構文木によれば、変換規則を定義する新たな抽象構文木のルールに沿った小さなプログラムを生成
- バベル新しいコードは、抽象構文木で、この時間は、コードがアプレットのルールに沿ったものである新しいコードに応じて生成されます
たとえば、太郎はバベル完全な構文変換と小さなプログラムです。
私たちは理解してここでは、変換キーコードは、私たちは抽象構文木の新世代を定義したルールに現在の抽象構文木に基づいて、変換プロセスは、新たな抽象構文木のプロセスを生成することです。
(ウォーカートラバーサーを達成するために)抽象構文木をトラバース
抽象構文木は、木のデータ構造であり、我々は、我々はAST上のノードにアクセスする必要がある、新しい構文木を生成したい、私たちは、抽象構文木のノードをトラバースするためのツールを必要としています。
const traverser = (ast, visitor) => {
// 如果节点是数组那么遍历数组
const traverseArray = (array, parent) => {
array.forEach((child) => {
traverseNode(child, parent);
});
};
// 遍历 ast 节点
const traverseNode = (node, parent) => {
const method = visitor[node.type];
if (method) {
method(node, parent);
}
switch (node.type) {
case 'Program':
traverseArray(node.body, node);
break;
case 'VariableDeclaration':
traverseArray(node.init.params, node.init);
break;
case 'identifier':
break;
default:
throw new TypeError(node.type);
}
};
traverseNode(ast, null);
};
コード変換(コンバータ実装トランス)
私たちは、コードを変換したいconst add = (a, b) => a + b
論理的に、我々はまた、このような変数宣言、あるべきES5コードに変換するに言えば、実際には変数宣言です。
var add = function(a, b) {
return a + b
}
もちろん、あなたが直接このような関数宣言を生成し、ルールに従うことはできません。
function add(a, b) {
return a + b
}
今回はES5の関数宣言のコードを変換します
我々の以前の歩行者はtraverser
、2つのパラメータを受け付ける1は、一人の訪問者、ASTノードオブジェクトであり、名前は、それが尊重に基づいて、次に、AST上の各ノードにアクセスする通り訪問者は、異なる方法、また訪問者として知られている訪問者をマウントするために、本質的にはJavaScriptのオブジェクトであります異なるノードが異なる変換方法を行うことに対応します。
const transformer = (ast) => {
// 新 ast
const newAst = {
type: 'Program',
body: []
};
// 在老 ast 上加一个指针指向新 ast
ast._context = newAst.body;
traverser(ast, {
// 对于变量声明的处理方法
VariableDeclaration: (node, parent) => {
let functionDeclaration = {
params: []
};
if (node.init.type === 'ArrowFunctionExpression') {
functionDeclaration.type = 'FunctionDeclaration';
functionDeclaration.identifierName = node.identifierName;
}
if (node.init.body.type === 'BinaryExpression') {
functionDeclaration.body = {
type: 'BlockStatement',
body: [{
type: 'ReturnStatement',
argument: node.init.body
}],
};
}
parent._context.push(functionDeclaration);
},
//对于字符的处理方法
identifier: (node, parent) => {
if (parent.type === 'ArrowFunctionExpression') {
// 忽略我这暴力的操作....领略大意即可..
ast._context[0].params.push({
type: 'identifier',
identifierName: node.identifierName
});
}
}
});
return newAst;
};
コードを生成する(実施ジェネレータジェネレータ)
我々は、我々は機能を実装するには、このステップは、実際に私たちの変換後に抽象構文木に基づいて、新たなコードを生成するためのコードを生成し、前に述べた、彼は再帰によって生成されたオブジェクト(AST)、最終的なコードを受け入れます
const generator = (node) => {
switch (node.type) {
// 如果是 `Program` 结点,那么我们会遍历它的 `body` 属性中的每一个结点,并且递归地
// 对这些结点再次调用 codeGenerator,再把结果打印进入新的一行中。
case 'Program':
return node.body.map(generator)
.join('\n');
// 如果是FunctionDeclaration我们分别遍历调用其参数数组以及调用其 body 的属性
case 'FunctionDeclaration':
return 'function' + ' ' + node.identifierName + '(' + node.params.map(generator) + ')' + ' ' + generator(node.body);
// 对于 `Identifiers` 我们只是返回 `node` 的 identifierName
case 'identifier':
return node.identifierName;
// 如果是BlockStatement我们遍历调用其body数组
case 'BlockStatement':
return '{' + node.body.map(generator) + '}';
// 如果是ReturnStatement我们调用其 argument 的属性
case 'ReturnStatement':
return 'return' + ' ' + generator(node.argument);
// 如果是ReturnStatement我们调用其左右节点并拼接
case 'BinaryExpression':
return generator(node.left) + ' ' + node.operator + ' ' + generator(node.right);
// 没有符合的则报错
default:
throw new TypeError(node.type);
}
};
これまでのところ、我々は、簡単なミニバベルを完了している、我々はテストを開始しました:
const compiler = (input) => {
const tokens = tokenizer(input);
const ast = parser(tokens);
const newAst = transformer(ast);
const output = generator(newAst);
return output;
};
const str = 'const add = (a, b) => a + b';
const result = compiler(str);
console.log(result);
// function add(a,b) {return a + b}
私たちは、のES6矢印関数function関数ES5を変換することに成功しました。
遂に
あなたが興味のletコンパイラの理論であり、中に深く行く方が良いです場合我々は、コードの数百行を使用し、バベルパッケージは、コードの巨大なプロジェクトの行数十万人の集まりで、このミニバベルのバベルの作品を理解することができますあなたが本当にバベル歓迎読者の理解したい場合は、多くの不合理があるコードの最も基本的な原則をだけを展示ソースを。
フロントエンドは、私たちの共通のES6変換ツールのバベル、コード検出eslintに加えて、多くのものがあり、関連する原則をコンパイルするために使用することができ、その上、我々はまた、次のことができます。
- 脱出マルチポートアプレット太郎
- アプレットのホットアップデートJSインタプリタ
- バベルとエラー監視ブラウザのJavaScriptの例外監視
- テンプレートエンジン
- 前処理などのCSSの後
- ...
この記事で、超小型のコンパイラインスピレーションを得ました。
いいえ公共ありません
私は、最新記事や最新のドキュメントの更新は、公開番号に注意してくださいリアルタイムに集中したいプログラマの面接、フォローアップ記事では、公開番号を更新するために優先させて頂きます。
テンプレートを再開します。国民の関心号返信「テンプレート」を取得
「フロントエンド面接マニュアル」:暴行マニュアルには、このガイドをサポートする「供給」、国民の関心号応答を取得