是否可以直接评估Mapbox表达式?
我正在寻找一种 JavaScript 表达式语法来指定 JSON 中的操作。Mapbox 的表达式正是我正在寻找的,但我找不到任何关于这些表达式是否可以在 Mapbox 之外使用的文档。那可能吗?如果是这样,你会怎么做?
回答
它们“只是”抽象语法树的 JSON 形式,因此您可以编写自己的执行程序。特别是,根据他们自己的文档,他们似乎遵循以下约定:
- 数组是表达式,而所有其他 JSON 类型都是文字(奇怪的是,这意味着没有直接使用数组文字!我稍后会详细说明)
- 数组的第一项是要执行的函数,其余项是该函数的参数。
- 根对象不一定与表达式语法相关,只是在它们发生的地方使用它。
- 唯一的“有状态”的是
let
/var
函数,它允许您创建变量,范围限定为封闭let
表达式,这表明它们可以通过某种方式将上下文传递给函数。
所以,让我们建立一个!我将尝试逐行查看下面的代码,但如果您更喜欢那里的格式,您也可以查看问题末尾的代码段源。
在这里,我们稍后将定义可用于表达式语言的所有函数
const OPERATIONS = {};
const OPERATIONS = {};
现在,让我们设置评估器功能。它显然必须接收它将评估的表达式,而且还必须接
const evaluate = (expression, context = {}) => {
收可以通过操作修改的上下文。
const evaluate = (expression,
if (!(expression instanceof Array)) {
return expression;
}
if (!(expression instanceof Array)) {
return expression;
}
context = {}) => {
首先,我们通过将它们作为它们本身来评估它们来处理文字
是的,现在是真正的交易:让我们找出要运行的操作及其参数。
const [operationKey, ...rawParameters] = expression;
const operation = OPERATIONS[operationKey];
我们通过恐慌来处理未知的操作!天啊!
if (operation == null) {
throw new Error(`Unknown operation ${operationKey}!`);
}
哦,太好了,我们知道这个操作!现在,我们应该如何称呼它?它显然需要接收其参数以及上下文,以防它是那些讨厌的有状态操作之一。另外,正如我们在 Mapbox 中看到的let
,操作可以创建新的上下文!
我建议使用以下签名,但您可以根据自己的特定偏好和用例进行更改:
第一个参数:
当前上下文
第二个参数:
所有操作参数的数组。如果操作是可变的,这使得迭代变得容易,并且更简单的东西仍然可以使用解构来获得“固定”签名。我们将传递参数“原始”,而不是评估,以便操作可以对它们做任何它想做的坏事。
返回值:
无论操作想要评估什么!
return operation(context, rawParameters);
};
对对对,我们已经设置了评估器,但是我们如何实际使用它呢?
我们需要一些操作,让我们从容易弄湿我们的脚开始:还记得我上面说过参数数组是原始的吗?我们需要在我们的操作函数中手动评估它们。
OPERATIONS["-"] = (context, [a, b]) => evaluate(a, context) - evaluate(b, context);
OPERATIONS["+"] = (context, [a, b]) => evaluate(a, context) + evaluate(b, context);
好的,这很容易,但是如果我们想接受任意数量的参数怎么办?
OPERATIONS["*"] = (context, parameters) => parameters
.map(p => evaluate(p, context))
.reduce((accumulator, x) => accumulator * x);
好的,现在让我们实现我们所说的那些数组。解决方案很简单,有一个操作,从它的参数创建数组!
OPERATIONS["array"] = (context, parameters) => parameters
.map(p => evaluate(p, context));
很酷,很酷,但是撒旦本人的邪恶后代呢?let
和var
?
让我们从其中较小的开始:简单,我们只是读取存储在上下文中的变量名!
OPERATIONS["var"] = (context, [variable]) => context[variable];
现在,“棘手”的 ,let
既是可变参数又改变了上下文!
我会在这里拔掉我的牙套,因为它会比以前漂亮的单线操作大一点!
OPERATIONS["let"] = (context, [...definitions]) => {
对,我们有 A 上下文,但我们不想在let
块外污染它!因此,让我们将其复制到一个新的临时文件中:
const innerContext = { ...context };
现在我们需要循环定义,记住,每个定义有 2 个元素:一个变量名,和它的值表达式!但首先,我们需要挑选最后一个参数,即要在结果上下文中执行的表达式:
const body = definitions.pop()
让我们把明显的东西放在一边,如果我们的定义中有奇数个东西,那么用户就错了!让我们把它扔在他们丑陋的脸上!让我们使用一个神秘的错误信息只是为了邪恶......
if (definitions.length % 2 === 1) {
throw new Error("Unmatched definitions!");
}
很酷,现在我们要做一些很酷的事情,即创建这些变量:
for (let i = 0; i < definitions.length - 1; i += 2) {
const name = definitions[i];
const value = definitions[i + 1];
在这里,我选择了同一块中的变量可以依赖于以前的变量,如果这不符合您的喜好,请使用父上下文而不是我们目前正在修改的上下文。
innerContext[name] = evaluate(value, innerContext);
}
变量已完成,现在让我们评估主体!
return evaluate(body, innerContext);
};
我们完成了!这是评估语法树的基础!
您现在可能想要继续添加您自己的特定于域的操作。
我制作了这个片段来演示这最终是如何工作的,如果这是您的风格,则使用代码注释而不是文字编码。HTML 和 CSS 无关紧要,只是涂了点口红让它看起来更像样。