Написано 2018.12.03
Что касается принципа составления, то зачастую впечатление остается только в скучных курсах и непонятных понятиях магистрантов. Нам, фронтенд-разработчикам, принцип компиляции кажется далеким, и наше понимание его, скорее всего, будет ограничено «Абстрактным синтаксическим деревом (AST)». Но это только начало. Использование принципа компиляции даже позволяет нам использовать JS для непосредственного написания интерпретатора, который может запускать код JS.
адрес проекта:GitHub.com/называется дождь AU/wipe…
онлайн опыт:код спрей.IO/ называется дождь AU/PE...
1. Зачем использовать JS для написания интерпретаторов JS
Учащиеся, принимавшие участие в разработке небольших программ, должны знать, что среда, в которой выполняются небольшие программы, запрещена.new Function
,eval
Использование таких методов делает невозможным непосредственное выполнение динамического кода в виде строк. Кроме того, многие платформы также ограничивают способ, которым эти JS могут выполнять динамический код, поэтому у нас нет возможности? В этом случае мы можем написать парсер на JS и позволить JS работать самому.
Прежде чем мы начнем, давайте кратко рассмотрим некоторые концепции принципов компиляции.
2. Что такое компилятор
Что касается принципа компиляции, то он должен быть неотделим от компилятора. Проще говоря, когда фрагмент кода проходит лексический анализ, синтаксический анализ и другие этапы компиляции, он генерирует древовидное «Абстрактное синтаксическое дерево (AST)», и каждый узел синтаксического дерева соответствует другому смысл в коде.фрагмент.
Например, есть такой кусок кода:
const a = 1
console.log(a)
После обработки компилятором его AST выглядит так:
{
"type": "Program",
"start": 0,
"end": 26,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 11,
"declarations": [
{
"type": "VariableDeclarator",
"start": 6,
"end": 11,
"id": {
"type": "Identifier",
"start": 6,
"end": 7,
"name": "a"
},
"init": {
"type": "Literal",
"start": 10,
"end": 11,
"value": 1,
"raw": "1"
}
}
],
"kind": "const"
},
{
"type": "ExpressionStatement",
"start": 12,
"end": 26,
"expression": {
"type": "CallExpression",
"start": 12,
"end": 26,
"callee": {
"type": "MemberExpression",
"start": 12,
"end": 23,
"object": {
"type": "Identifier",
"start": 12,
"end": 19,
"name": "console"
},
"property": {
"type": "Identifier",
"start": 20,
"end": 23,
"name": "log"
},
"computed": false
},
"arguments": [
{
"type": "Identifier",
"start": 24,
"end": 25,
"name": "a"
}
]
}
}
],
"sourceType": "module"
}
Общие компиляторы JS:
babylon
,acorn
Подождите, заинтересованные студенты могут перейти кAST explorerИспытайте этот сайт на себе.
Видно, что скомпилированный AST подробно записывает тип, начальную позицию и другую информацию всех семантических кодов в коде. Этот код, кроме корневого узлаProgram
Кроме того, основной корпус содержит два узлаVariableDeclaration
а такжеExpressionStatement
, и эти узлы содержат разные дочерние узлы.
Именно потому, что AST подробно записывает семантическую информацию кода, Babel, Webpack, Sass, Less и другие инструменты могут очень разумно обрабатывать код.
3. Что такое переводчик
Подобно тому, как переводчик может не только понимать иностранный язык, но и переводить его на родной язык после художественной обработки, инструмент, способный преобразовать код в АСТ, люди называют «компилятором», а инструмент, способный переводить АСТ на целевой язык называется "компилятором", а инструмент, который его запускает, называется "интерпретатором".
В ходе принципа компиляции мы задумались над такой проблемой: как заставить компьютер выполнять арифметическое выражение1+2+3
:
1 + 2 + 3
Когда машина выполняет его, это может выглядеть как этот машинный код:
1 PUSH 1
2 PUSH 2
3 ADD
4 PUSH 3
5 ADD
Программа, которая запускает этот машинный код, является интерпретатором.
В этой статье мы не будем придумывать что-то настолько сложное, как машинный код, просто используем JS для интерпретации AST кода JS в его среде выполнения. Так как интерпретатор написан на JS, мы можем смело использовать языковые возможности JS, такие как эта привязка, новое ключевое слово и т.д., без какой-либо дополнительной обработки над ними, что делает реализацию интерпретатора JS очень простой.
Ознакомившись с основными понятиями принципов компиляции, можно приступать к разработке.
В-четвертых, обходчик узла
Анализируя приведенный выше AST, вы можете видеть, что каждый узел будет иметь атрибут типаtype
, для разных типов узлов требуются разные методы обработки, и программа для обработки этих узлов — это «процессор узлов (nodeHandler
)”
Определите обработчик узла:
const nodeHandler = {
Program () {},
VariableDeclaration () {},
ExpressionStatement () {},
MemberExpression () {},
CallExpression () {},
Identifier () {}
}
Конкретная реализация узлового процессора будет подробно рассмотрена позже и пока не будет здесь подробно останавливаться.
С процессором узлов нам нужно пройти каждый узел в AST и рекурсивно вызывать процессор узлов, пока не будет обработана вся книга грамматики.
Определите итератор узла (NodeIterator
):
class NodeIterator {
constructor (node) {
this.node = node
this.nodeHandler = nodeHandler
}
traverse (node) {
// 根据节点类型找到节点处理器当中对应的函数
const _eval = this.nodeHandler[node.type]
// 若找不到则报错
if (!_eval) {
throw new Error(`canjs: Unknown node type "${node.type}".`)
}
// 运行处理函数
return _eval(node)
}
}
Теоретически обходчик узлов можно спроектировать так, но после тщательного изучения обнаруживается, что упущена очень важная вещь — обработка области видимости.
вернуться к процессору узлаVariableDeclaration()
метод, который обрабатывает такие вещи, какconst a = 1
Такой узел объявления переменных. Предположим, что его код выглядит следующим образом:
VariableDeclaration (node) {
for (const declaration of node.declarations) {
const { name } = declaration.id
const value = declaration.init ? traverse(declaration.init) : undefined
// 问题来了,拿到了变量的名称和值,然后把它保存到哪里去呢?
// ...
}
},
Проблема в том, что после обработки узла объявления переменной переменная должна быть сохранена. В соответствии с особенностями языка JS эта переменная должна храниться в области видимости. В реализации парсера JS эта область может быть определена какscope
объект.
Перепишите обходчик узла, чтобы добавить к нему новый.scope
объект
class NodeIterator {
constructor (node, scope = {}) {
this.node = node
this.scope = scope
this.nodeHandler = nodeHandler
}
traverse (node, options = {}) {
const scope = options.scope || this.scope
const nodeIterator = new NodeIterator(node, scope)
const _eval = this.nodeHandler[node.type]
if (!_eval) {
throw new Error(`canjs: Unknown node type "${node.type}".`)
}
return _eval(nodeIterator)
}
createScope (blockType = 'block') {
return new Scope(blockType, this.scope)
}
}
Затем функция обработчика узлаVariableDeclaration()
ты можешь пройтиscope
Сохраните переменную:
VariableDeclaration (nodeIterator) {
const kind = nodeIterator.node.kind
for (const declaration of nodeIterator.node.declarations) {
const { name } = declaration.id
const value = declaration.init ? nodeIterator.traverse(declaration.init) : undefined
// 在作用域当中定义变量
// 如果当前是块级作用域且变量用var定义,则定义到父级作用域
if (nodeIterator.scope.type === 'block' && kind === 'var') {
nodeIterator.scope.parentScope.declare(name, value, kind)
} else {
nodeIterator.scope.declare(name, value, kind)
}
}
},
Обработка областей действия, пожалуй, самая сложная часть всего интерпретатора JS. Далее мы углубимся в работу с прицелом.
Пять, обработка прицела
Рассмотрим такую ситуацию:
const a = 1
{
const b = 2
console.log(a)
}
console.log(b)
Результат операции должен иметь возможность распечататьa
значение, а затем сообщается об ошибке:Uncaught ReferenceError: b is not defined
Этот код связан с масштабом проблемы. Область блока или область функции могут читать переменные в своей родительской области, но не наоборот, поэтому мы не можем просто определить пустой объект для областей видимости, а работать с ними особым образом.
Определить базовый класс областиScope
:
class Scope {
constructor (type, parentScope) {
// 作用域类型,区分函数作用域function和块级作用域block
this.type = type
// 父级作用域
this.parentScope = parentScope
// 全局作用域
this.globalDeclaration = standardMap
// 当前作用域的变量空间
this.declaration = Object.create(null)
}
/*
* get/set方法用于获取/设置当前作用域中对应name的变量值
符合JS语法规则,优先从当前作用域去找,若找不到则到父级作用域去找,然后到全局作用域找。
如果都没有,就报错
*/
get (name) {
if (this.declaration[name]) {
return this.declaration[name]
} else if (this.parentScope) {
return this.parentScope.get(name)
} else if (this.globalDeclaration[name]) {
return this.globalDeclaration[name]
}
throw new ReferenceError(`${name} is not defined`)
}
set (name, value) {
if (this.declaration[name]) {
this.declaration[name] = value
} else if (this.parentScope[name]) {
this.parentScope.set(name, value)
} else {
throw new ReferenceError(`${name} is not defined`)
}
}
/**
* 根据变量的kind调用不同的变量定义方法
*/
declare (name, value, kind = 'var') {
if (kind === 'var') {
return this.varDeclare(name, value)
} else if (kind === 'let') {
return this.letDeclare(name, value)
} else if (kind === 'const') {
return this.constDeclare(name, value)
} else {
throw new Error(`canjs: Invalid Variable Declaration Kind of "${kind}"`)
}
}
varDeclare (name, value) {
let scope = this
// 若当前作用域存在非函数类型的父级作用域时,就把变量定义到父级作用域
while (scope.parentScope && scope.type !== 'function') {
scope = scope.parentScope
}
this.declaration[name] = new SimpleValue(value, 'var')
return this.declaration[name]
}
letDeclare (name, value) {
// 不允许重复定义
if (this.declaration[name]) {
throw new SyntaxError(`Identifier ${name} has already been declared`)
}
this.declaration[name] = new SimpleValue(value, 'let')
return this.declaration[name]
}
constDeclare (name, value) {
// 不允许重复定义
if (this.declaration[name]) {
throw new SyntaxError(`Identifier ${name} has already been declared`)
}
this.declaration[name] = new SimpleValue(value, 'const')
return this.declaration[name]
}
}
Здесь метод называетсяsimpleValue()
функция для определения значений переменных, в основном используемая для работы с константами:
class SimpleValue {
constructor (value, kind = '') {
this.value = value
this.kind = kind
}
set (value) {
// 禁止重新对const类型变量赋值
if (this.kind === 'const') {
throw new TypeError('Assignment to constant variable')
} else {
this.value = value
}
}
get () {
return this.value
}
}
Ключ к решению проблемы области видимости заключается в том, что язык JS сам находит характеристики переменных — текущая область действия имеет приоритет, родительская область — вторая, а глобальная область — последняя. В свою очередь, обработайте функцию в узлеVariableDeclaration()
, если встречается область блока и ключевое словоvar
, вам нужно определить эту переменную в родительской области, что мы часто называем «загрязнением глобальной переменной».
Внедрение стандартной библиотеки JS
Внимательный читатель увидит, что в определенииScope
Когда базовый класс, его глобальная областьglobalScope
назначенstandardMap
Object, этот объект является стандартной библиотекой JS.
Проще говоря, стандартная библиотека JS — это набор методов и свойств, которыми обладает сам язык JS, например, обычно используемыеsetTimeout
,console.log
и т.п. Чтобы парсер тоже выполнял эти методы, нам нужно внедрить в него стандартную библиотеку:
const standardMap = {
console: new SimpleValue(console)
}
Это эквивалентно внедрению в глобальную область парсера.console
Этот объект также можно использовать напрямую.
6. Процессор узла
После работы с обходчиком узла и обработкой области пришло время написать обработчик узла. Как следует из названия, процессор узла предназначен для обработки узлов AST, как неоднократно упоминалось выше.VariableDeclaration()
Метод является одним из них. Далее будут объяснены некоторые процессоры ключевых узлов.
Прежде чем разрабатывать процессор узла, вам нужно использовать инструмент для оценки оператора JS.return
,break
,continue
ключевые слова.
Инструмент оценки ключевых словSignal
определитьSignal
Базовый класс:
class Signal {
constructor (type, value) {
this.type = type
this.value = value
}
static Return (value) {
return new Signal('return', value)
}
static Break (label = null) {
return new Signal('break', label)
}
static Continue (label) {
return new Signal('continue', label)
}
static isReturn(signal) {
return signal instanceof Signal && signal.type === 'return'
}
static isContinue(signal) {
return signal instanceof Signal && signal.type === 'continue'
}
static isBreak(signal) {
return signal instanceof Signal && signal.type === 'break'
}
static isSignal (signal) {
return signal instanceof Signal
}
}
С его помощью вы сможете судить и обрабатывать ключевые слова в выписке, что очень пригодится в будущем.
1. Процессор узла определения переменной——VariableDeclaration()
Один из наиболее часто используемых обработчиков узлов, отвечающий за регистрацию переменных в правильной области видимости.
VariableDeclaration (nodeIterator) {
const kind = nodeIterator.node.kind
for (const declaration of nodeIterator.node.declarations) {
const { name } = declaration.id
const value = declaration.init ? nodeIterator.traverse(declaration.init) : undefined
// 在作用域当中定义变量
// 若为块级作用域且关键字为var,则需要做全局污染
if (nodeIterator.scope.type === 'block' && kind === 'var') {
nodeIterator.scope.parentScope.declare(name, value, kind)
} else {
nodeIterator.scope.declare(name, value, kind)
}
}
},
2. Процессор узла идентификатора——Identifier()
В частности, используется для получения значения идентификатора из области.
Identifier (nodeIterator) {
if (nodeIterator.node.name === 'undefined') {
return undefined
}
return nodeIterator.scope.get(nodeIterator.node.name).value
},
3. Процессор узла персонажа——Literal()
Возвращает значение узла персонажа.
Literal (nodeIterator) {
return nodeIterator.node.value
}
4. Выражение вызывает процессор узла -CallExpression()
Обработчик для обработки узлов вызова выражений, таких как обработкаfunc()
,console.log()
Ждать.
CallExpression (nodeIterator) {
// 遍历callee获取函数体
const func = nodeIterator.traverse(nodeIterator.node.callee)
// 获取参数
const args = nodeIterator.node.arguments.map(arg => nodeIterator.traverse(arg))
let value
if (nodeIterator.node.callee.type === 'MemberExpression') {
value = nodeIterator.traverse(nodeIterator.node.callee.object)
}
// 返回函数运行结果
return func.apply(value, args)
},
5. Процессор узла экспрессии——MemberExpression()
В отличие от вышеприведенного «обработчика узла вызова выражения», узел выражения относится кperson.say
,console.log
Это функциональное выражение.
MemberExpression (nodeIterator) {
// 获取对象,如console
const obj = nodeIterator.traverse(nodeIterator.node.object)
// 获取对象的方法,如log
const name = nodeIterator.node.property.name
// 返回表达式,如console.log
return obj[name]
}
6, процессор узла операторов блочного уровня -BlockStatement()
Очень распространенный процессор, специализирующийся на обработке узлов объявления блочного уровня, таких как функции, циклы,try...catch...
сцена в нем.
BlockStatement (nodeIterator) {
// 先定义一个块级作用域
let scope = nodeIterator.createScope('block')
// 处理块级节点内的每一个节点
for (const node of nodeIterator.node.body) {
if (node.type === 'VariableDeclaration' && node.kind === 'var') {
for (const declaration of node.declarations) {
scope.declare(declaration.id.name, declaration.init.value, node.kind)
}
} else if (node.type === 'FunctionDeclaration') {
nodeIterator.traverse(node, { scope })
}
}
// 提取关键字(return, break, continue)
for (const node of nodeIterator.node.body) {
if (node.type === 'FunctionDeclaration') {
continue
}
const signal = nodeIterator.traverse(node, { scope })
if (Signal.isSignal(signal)) {
return signal
}
}
}
Вы можете видеть, что этот процессор имеет дваfor...of
цикл. Первый используется для обработки операторов на уровне блока, а второй предназначен для идентификации ключевых слов, например, внутри тела цикла.break
,continue
или внутри тела функцииreturn
.
7. Процессор узла определения функций——FunctionDeclaration()
Объявите в действии переменную с тем же именем, что и функция, и значением будет определенная функция:
FunctionDeclaration (nodeIterator) {
const fn = NodeHandler.FunctionExpression(nodeIterator)
nodeIterator.scope.varDeclare(nodeIterator.node.id.name, fn)
return fn
}
8. Процессор функционального узла выражения——FunctionExpression()
Используется для определения функции:
FunctionExpression (nodeIterator) {
const node = nodeIterator.node
/**
* 1、定义函数需要先为其定义一个函数作用域,且允许继承父级作用域
* 2、注册`this`, `arguments`和形参到作用域的变量空间
* 3、检查return关键字
* 4、定义函数名和长度
*/
const fn = function () {
const scope = nodeIterator.createScope('function')
scope.constDeclare('this', this)
scope.constDeclare('arguments', arguments)
node.params.forEach((param, index) => {
const name = param.name
scope.varDeclare(name, arguments[index])
})
const signal = nodeIterator.traverse(node.body, { scope })
if (Signal.isReturn(signal)) {
return signal.value
}
}
Object.defineProperties(fn, {
name: { value: node.id ? node.id.name : '' },
length: { value: node.params.length }
})
return fn
}
9, этот процессор выражений -ThisExpression()
Процессор напрямую использует характеристики самого языка JS, чтобы поставитьthis
Ключевое слово может быть удалено из области видимости.
ThisExpression (nodeIterator) {
const value = nodeIterator.scope.get('this')
return value ? value.value : null
}
10, новый процессор выражений -NewExpression()
а такжеthis
Подобно выражениям, он также напрямую следует языковым особенностям JS.После получения функций и параметров передайтеbind
Ключевое слово создает конструктор и возвращает значение.
NewExpression (nodeIterator) {
const func = nodeIterator.traverse(nodeIterator.node.callee)
const args = nodeIterator.node.arguments.map(arg => nodeIterator.traverse(arg))
return new (func.bind(null, ...args))
}
11. Для процессора узлов контура——ForStatement()
Три параметра цикла For соответствуютinit
,test
,update
Атрибуты, вызовите процессор узла для обработки трех атрибутов соответственно и поместите их обратно в собственный цикл for JS.
ForStatement (nodeIterator) {
const node = nodeIterator.node
let scope = nodeIterator.scope
if (node.init && node.init.type === 'VariableDeclaration' && node.init.kind !== 'var') {
scope = nodeIterator.createScope('block')
}
for (
node.init && nodeIterator.traverse(node.init, { scope });
node.test ? nodeIterator.traverse(node.test, { scope }) : true;
node.update && nodeIterator.traverse(node.update, { scope })
) {
const signal = nodeIterator.traverse(node.body, { scope })
if (Signal.isBreak(signal)) {
break
} else if (Signal.isContinue(signal)) {
continue
} else if (Signal.isReturn(signal)) {
return signal
}
}
}
По аналогии,for...in
,while
а такжеdo...while
Цикл также обрабатывается аналогичным образом и здесь повторяться не будет.
12. Если объявляется процессор узла -IfStatemtnt()
Обработка операторов If, включаяif
,if...else
,if...elseif...else
.
IfStatement (nodeIterator) {
if (nodeIterator.traverse(nodeIterator.node.test)) {
return nodeIterator.traverse(nodeIterator.node.consequent)
} else if (nodeIterator.node.alternate) {
return nodeIterator.traverse(nodeIterator.node.alternate)
}
}
По аналогии,switch
Операторы и тернарные выражения обрабатываются аналогично.
---
Несколько важных процессоров нод перечислены выше.В es5 еще много нод, которые необходимо обработать.Подробности можно найтиЭтот адреспроверить это.
7. Определите метод вызова
После всех вышеперечисленных шагов синтаксический анализатор имеет возможность обрабатывать код es5, следующим шагом является сборка этого массового содержимого и, наконец, определение метода, удобного для вызова пользователями.
const { Parser } = require('acorn')
const NodeIterator = require('./iterator')
const Scope = require('./scope')
class Canjs {
constructor (code = '', extraDeclaration = {}) {
this.code = code
this.extraDeclaration = extraDeclaration
this.ast = Parser.parse(code)
this.nodeIterator = null
this.init()
}
init () {
// 定义全局作用域,该作用域类型为函数作用域
const globalScope = new Scope('function')
// 根据入参定义标准库之外的全局变量
Object.keys(this.extraDeclaration).forEach((key) => {
globalScope.addDeclaration(key, this.extraDeclaration[key])
})
this.nodeIterator = new NodeIterator(null, globalScope)
}
run () {
return this.nodeIterator.traverse(this.ast)
}
}
Здесь мы определяемCanjs
Базовый класс принимает код JS в виде строк и может определять переменные вне стандартной библиотеки. при бегеrun()
метод, вы можете получить результат операции.
8. Последующие действия
На данный момент весь синтаксический анализатор JS завершен и может хорошо запускать код ES5 (могут быть ошибки, которые не были обнаружены). Но в текущей реализации все запущенные результаты помещаются в песочницу и не могут влиять на внешний мир. Если вы хотите вывести текущий результат, есть два возможных способа. Первый — передать глобальную переменную, применить влияние к этой глобальной переменной и использовать ее для вывода результата; второй — позволить синтаксическому анализатору поддерживатьexport
грамматика, умеетexport
Результат оператора оператора возвращается, и заинтересованные читатели могут изучить его самостоятельно.
Наконец, этот парсер JS был открыт на моем Github, добро пожаловать в общение~
GitHub.com/называется дождь AU/wipe…