Недавно исследуя AST, есть статья передИнтервьюер: Вы знаете Бабеля? Вы написали плагин для Babel? Ответ: Нет. умерЗачем об этом узнавать, потому что знание AST действительно может делать все, что вы хотите.
Проще говоря, используйте Javascript для запуска кода Javascript.
Эта статья расскажет вам, как написать простой парсер.
Предисловие (пропустите, если знаете, как выполнять пользовательский код js)
На ваш взгляд, есть несколько способов выполнения пользовательских скриптов? Давайте перечислим:
Web
Создайте сценарий сценария и вставьте его в поток документов
function runJavascriptCode(code) {
const script = document.createElement("script");
script.innerText = code;
document.body.appendChild(script);
}
runJavascriptCode("alert('hello world')");
eval
Бесчисленное количество людей говорят, не используйтеeval
, хотя он может выполнять пользовательские сценарии
eval("alert('hello world')");
Ссылка на ссылку:Why is using the JavaScript eval function a bad idea?
setTimeout
setTimeout также может быть выполнен, но связанные операции будут перемещены в следующий цикл обработки событий для выполнения.
setTimeout("console.log('hello world')");
console.log("I should run first");
// 输出
// I should run first
// hello world'
new Function
new Function("alert('hello world')")();
Ссылка на ссылку:Are eval() and new Function() the same thing?
NodeJs
require
Вы можете записать код Javascript в файл Js, а затем потребовать его в других файлах для достижения эффекта выполнения.
NodeJs будет кэшировать модули, если вы выполняете N таких файлов, это может потреблять много памяти, вам нужно очистить кеш вручную после выполнения.
Vm
const vm = require("vm");
const sandbox = {
animal: "cat",
count: 2
};
vm.runInNewContext('count += 1; name = "kitty"', sandbox);
Вышеупомянутые методы, за исключением Node, могут выполняться изящно, кроме этого, API должен зависеть от среды хоста.
Использование интерпретатора
Выполняйте пользовательский код на любой платформе, которая может выполнять код Javascript.
Например, апплет блокирует описанный выше способ выполнения пользовательского кода.
Так действительно ли невозможно выполнить пользовательский код?
не также
Принцип работы
На основе AST (абстрактного синтаксического дерева) найдите соответствующий объект/метод, а затем выполните соответствующее выражение.
Как сказать, это немного запутанно, дайте каштанconsole.log("hello world");
Принцип: найти по АСТconsole
объект, найти его сноваlog
функцию и, наконец, запустите функцию, параметрыhello world
инструменты для подготовки
- Babylon, для разбора кода, генерации AST
- babel-типы, чтобы определить тип узла
- astexplorer, чтобы просмотреть абстрактное синтаксическое дерево в любое время
начать кодирование
мы бежимconsole.log("hello world")
Например
Открытьastexplorer, просмотреть соответствующий AST
Как видно из рисунка, нужно найтиconsole.log("hello world")
, необходимо пройти узел вниз, послеFile
,Program
,ExpressionStatement
,CallExpression
,MemberExpression
узел, который включаетIdentifier
,StringLiteral
узел
Сначала мы определяемvisitors
, visitors
это метод обработки для разных узлов
const visitors = {
File(){},
Program(){},
ExpressionStatement(){},
CallExpression(){},
MemberExpression(){},
Identifier(){},
StringLiteral(){}
};
Определите функцию, которая проходит через узлы
/**
* 遍历一个节点
* @param {Node} node 节点对象
* @param {*} scope 作用域
*/
function evaluate(node, scope) {
const _evalute = visitors[node.type];
// 如果该节点不存在处理函数,那么抛出错误
if (!_evalute) {
throw new Error(`Unknown visitors of ${node.type}`);
}
// 执行该节点对应的处理函数
return _evalute(node, scope);
}
Ниже приведена реализация обработки каждого узла.
const babylon = require("babylon");
const types = require("babel-types");
const visitors = {
File(node, scope) {
evaluate(node.program, scope);
},
Program(program, scope) {
for (const node of program.body) {
evaluate(node, scope);
}
},
ExpressionStatement(node, scope) {
return evaluate(node.expression, scope);
},
CallExpression(node, scope) {
// 获取调用者对象
const func = evaluate(node.callee, scope);
// 获取函数的参数
const funcArguments = node.arguments.map(arg => evaluate(arg, scope));
// 如果是获取属性的话: console.log
if (types.isMemberExpression(node.callee)) {
const object = evaluate(node.callee.object, scope);
return func.apply(object, funcArguments);
}
},
MemberExpression(node, scope) {
const { object, property } = node;
// 找到对应的属性名
const propertyName = property.name;
// 找对对应的对象
const obj = evaluate(object, scope);
// 获取对应的值
const target = obj[propertyName];
// 返回这个值,如果这个值是function的话,那么应该绑定上下文this
return typeof target === "function" ? target.bind(obj) : target;
},
Identifier(node, scope) {
// 获取变量的值
return scope[node.name];
},
StringLiteral(node) {
return node.value;
}
};
function evaluate(node, scope) {
const _evalute = visitors[node.type];
if (!_evalute) {
throw new Error(`Unknown visitors of ${node.type}`);
}
// 递归调用
return _evalute(node, scope);
}
const code = "console.log('hello world')";
// 生成AST树
const ast = babylon.parse(code);
// 解析AST
// 需要传入执行上下文,否则找不到``console``对象
evaluate(ast, { console: console });
Попробуйте в Nodejs
$ node ./index.js
hello world
Затем мы меняем код для запускаconst code = "console.log(Math.pow(2, 2))";
потому что контекст неMath
объект, то вы получите такую ошибкуTypeError: Cannot read property 'pow' of undefined
Не забудьте передать в контекстеevaluate(ast, {console, Math});
Запустите его снова и получите другую ошибкуError: Unknown visitors of NumericLiteral
оказалосьMath.pow(2, 2)
2 в , является числовым литералом
Узел естьNumericLiteral
, Но когдаvisitors
, мы не определили, как следует обрабатывать этот узел.
Затем мы добавляем этот узел:
NumericLiteral(node){
return node.value;
}
Запустите его снова, он соответствует ожидаемому результату
$ node ./index.js
4
До сих пор был реализован самый простой вызов функции.
Передовой
Поскольку это интерпретатор, может ли он запускать только hello world? Очевидно нет
Давайте объявим переменную
var name = "hello world";
console.log(name);
Первый взгляд на структуру AST
visitors
отсутствует вVariableDeclaration
а такжеVariableDeclarator
Обработка узла, мы добавляем
VariableDeclaration(node, scope) {
const kind = node.kind;
for (const declartor of node.declarations) {
const {name} = declartor.id;
const value = declartor.init
? evaluate(declartor.init, scope)
: undefined;
scope[name] = value;
}
},
VariableDeclarator(node, scope) {
scope[node.id.name] = evaluate(node.init, scope);
}
Запустите следующий код, он был распечатанhello world
Давайте объявим функцию
function test() {
var name = "hello world";
console.log(name);
}
test();
В соответствии с вышеуказанными шагами было добавлено несколько новых узлов.
BlockStatement(block, scope) {
for (const node of block.body) {
// 执行代码块中的内容
evaluate(node, scope);
}
},
FunctionDeclaration(node, scope) {
// 获取function
const func = visitors.FunctionExpression(node, scope);
// 在作用域中定义function
scope[node.id.name] = func;
},
FunctionExpression(node, scope) {
// 自己构造一个function
const func = function() {
// TODO: 获取函数的参数
// 执行代码块中的内容
evaluate(node.body, scope);
};
// 返回这个function
return func;
}
затем изменитьCallExpression
// 如果是获取属性的话: console.log
if (types.isMemberExpression(node.callee)) {
const object = evaluate(node.callee.object, scope);
return func.apply(object, funcArguments);
} else if (types.isIdentifier(node.callee)) {
// 新增
func.apply(scope, funcArguments); // 新增
}
Бег также может распечататьhello world
разное
Из-за ограниченности места я не буду рассказывать, как работать со всеми узлами, основные принципы были объяснены выше.
Для других узлов вы все равно можете сделать это со следующими оговорками:В приведенном выше примере я использовал унифицированную область, в которой нет различий между родительской и дочерней областью.
Это означает, что код может быть запущен
var a = 1;
function test() {
var b = 2;
}
test();
console.log(b); // 2
Метод обработки: если при рекурсии дерева AST вы столкнетесь с некоторыми узлами, которые будут генерировать подобласти, вам следует использовать новую область, напримерfunction
,for in
Ждать
наконец
Вышеупомянутая просто модель, это даже не игрушка, ямок еще много. Например:
- Переменные улучшены, а область действия должна иметь предшественников.
- Сфера имеет много проблем
- Конкретный узел, который должен быть вложен в узел. Например, super() должен находиться внутри узла Class, независимо от того, сколько уровней вложенности.
- эта привязка
- ...
Пробыв несколько ночей подряд, я написал относительно полную библиотеку.vm.js, на основеjsjsМодифицированный, стоящий на плечах гигантов.
В отличие от него:
- Переработан рекурсивный способ решения некоторых неразрешимых проблем.
- Исправлено несколько ошибок
- Добавлены тестовые случаи
- Поддержка es6 и другого синтаксического сахара
В настоящее время он находится в стадии разработки, и первая версия будет выпущена после ожидания большего совершенства.
Добро пожаловать большие ребята, чтобы сделать кирпичи и PR.
В будущем маленькие программы станут большими программами, а бизнес-коды будут передаваться и выполняться через Websocket.Исходный код маленьких программ — это просто пустая оболочка. Думать об этом интересно.
адрес проекта:GitHub.com/A XE Трой/ВМ.…
онлайн просмотр:axetroy.github.io/vm.js/
оригинал:axetroy.xyz/#/post/172