TL; DR
VKScript 不是 JavaScript。 这种语言的语义从根本上不同于JavaScript的语义。 见结论 。
什么是VKScript?
VKScript是VKontakte execute
API方法中使用的一种类似于JavaScript的脚本编程语言,它使客户能够准确下载所需的信息。 本质上,VKScript是Facebook用于相同目的的GraphQL的类似物。
比较GraphQL和VKScript:
VK API文档 (唯一的官方语言文档)中的方法页面上的VKScript描述:
支持以下内容:
- 算术运算
- 逻辑运算
- 创建数组和列表([X,Y])
- parseInt和parseDouble
- 串联(+)
- 如果构造
- 按参数(@。)进行数组过滤
- API方法调用, 长度参数
- 使用while语句循环
- Javascript方法: slice , push , pop , shift , unshift , splice , substr , split
- 删除运算符
- 分配给数组元素,例如:row.user.action =“ test”;
- 数组或字符串中的搜索为indexOf ,例如:“ 123” .indexOf(2)= 1,[1、2、3] .indexOf(3)=2。如果未找到该元素,则返回-1。
当前不支持函数创建。
引用的文档指出“已计划ECMAScript兼容性”。 但是是这样吗? 让我们尝试从内部了解这种语言的工作方式。
目录内容
- VKScript虚拟机
- VKScript对象的语义
- 结论
VKScript虚拟机
在没有本地副本的情况下如何分析程序? 没错-将请求发送到公共端点并分析答案。 例如,让我们尝试执行以下代码:
while(1);
我们收到Runtime error occurred during code invocation: Too many operations
的Runtime error occurred during code invocation: Too many operations
。 这表明在执行该语言时,所执行的操作数是有限制的。 让我们尝试设置确切的极限值:
var i = 0; while(i < 1000) i = i + 1;
Runtime error occurred during code invocation: Too many operations
。
var i = 0; while(i < 999) i = i + 1;
{"response": null}
-代码成功执行。
因此,操作次数的限制约为1000个“空闲”周期。 但是,与此同时,很明显,这种循环很可能不是“单一”的操作。 让我们尝试找到一个不会被编译器分成几个较小的操作的操作。
此类操作最明显的候选者是所谓的空语句( ;
)。 但是,在将代码添加到i < 999
50个字符之后;
,不超过限制。 这意味着要么空语句由编译器抛出并且不浪费操作,要么循环的一次迭代需要50个以上的操作(很可能不是这样)。
之后想到的下一件事;
-计算一些简单的表达式(例如: 1;
)。 让我们尝试将其中一些表达式添加到我们的代码中:
var i = 0; while(i < 999) i = i + 1; 1;
因此,2个操作1;
花费的手术多于50次;
。 这证实了以下假设:空语句不会浪费指令。
让我们尝试减少循环的迭代次数,并增加1;
。 容易注意到,每次迭代有5个额外的1;
因此,循环的一次迭代所花的运算量比一个运算1;
多5倍1;
。
但是,有没有更简单的操作? 例如,添加一元运算符~
不需要计算其他表达式,并且运算本身在处理器上执行。 逻辑上假设将此操作添加到表达式中会使操作总数增加1。
将此运算符添加到我们的代码中:
var i = 0; while(i < 999) i = i + 1; ~1;
是的,我们可以添加一个这样的运算符,再添加一个表达式1;
-不再。 因此, 1;
确实不是单一运营商。
与运算符1;
类似1;
,我们将减少循环的迭代次数并添加~
运算符。 一次迭代等于10个unit运算~
,因此,表达式1;
花费2次操作。
请注意,该限制约为1000次迭代,即大约10,000次单个操作。 我们假设该限制恰好是10,000次操作。
测量代码中的操作数
请注意,现在我们可以测量任何代码中的操作数。 为此,请在循环后添加此代码,并添加/删除迭代, ~
运算符或最后一行,直到“ Too many operations
错误消失。
一些测量结果:
确定虚拟机的类型
首先,您需要了解VKScript解释器的工作方式。 有两种或多或少的合理选择:
- 解释器递归地遍历语法树并在每个节点上执行操作。
- 编译器将语法树转换为解释器执行的指令序列。
很容易理解,VKScript使用了第二个选项。 考虑一下表达式(true?1:1);
(5个操作)和(false?1:1);
(4次操作)。 在顺序执行指令的情况下,通过“绕过”错误的选项的转换来解释附加操作,在递归AST旁路的情况下,两个选项对于解释器都是等效的。 如果/否则,在不同条件下会观察到类似的效果。
另外,对i = 1;
也是值得关注的i = 1;
(3个运算),并且var j = 1;
(1次操作)。 创建一个新变量仅花费1个操作,而分配给一个现有变量则花费3个操作? 创建变量需要花费1次操作(并且很可能是恒定的加载操作),这一事实说明了两点:
- 创建新变量时,没有为该变量显式分配内存。
- 创建新变量时,该值不会加载到存储单元中。 这意味着将在计算表达式值的位置分配用于新变量的空间,然后将其视为已分配的内存。 这建议使用堆栈机。
使用堆栈还可以解释表达式var j = 1;
比表达式1;
运行更快1;
:最后一个表达式花费额外的指令从堆栈中删除计算值。
确定确切的极限值
注意循环var j=0;while(j < 1)j=j+1;
(15个操作)是用于测量的循环的一小部分副本:
停什么 是否有9998条指令的限制? 我们显然缺少了一些东西...
请注意, return 1;
码为return 1;
根据0条指令的测量结果执行。 这很容易解释:编译器在代码的末尾添加了一个隐式的return null;
,并且在添加返回值时失败。 假设限制为10000,我们得出的结论是该操作return null;
需要2条指令(可能这类似于push null; return;
)。
嵌套代码块
让我们进行更多测量:
让我们注意以下事实:
- 将变量添加到块中需要执行一项额外的操作。
- 当“再次声明变量”时,第二个声明作为常规分配完成。
- 但是同时,从外部看不到块内部的变量(请参见最后一个示例)。
很容易理解,花了额外的操作从堆栈中删除块中声明的局部变量。 因此,当没有局部变量时,不需要删除任何内容。
对象,方法,API调用
让我们分析结果。 您可能会注意到,创建一个字符串和一个空数组/对象需要2次操作,加载数字也一样。 创建非空数组或对象时,将添加花费在加载数组/对象元素上的操作。 这表明在一个操作中直接创建对象。 同时,不浪费时间下载属性名称;因此,下载属性名称是创建对象的操作的一部分。
使用API方法调用,一切也很平常-加载一个单元,实际调用该方法, pop
结果(您会注意到方法名称是作为一个整体处理的,而不是作为属性来处理的)。 但是最后三个示例看起来很有趣。
"".substr(0, 0);
-加载字符串,加载零,加载零, pop
结果。 由于某种原因,有2条关于调用方法的指令(出于某种原因,请参见下文)。var j={};jx=1;
-创建对象,加载对象,加载单元,分配后pop
单元。 同样,有2条分配说明。var j={x:1};delete jx;
-加载单元,创建对象,加载对象,删除。 每个删除操作有3条指令。
VKScript对象的语义
数字
回到原始问题:VKScript是JavaScript或另一种语言的子集吗? 让我们做一个简单的测试:
return 1000000000 + 2000000000;
{"response": -1294967296};
我们可以看到,尽管JavaScript没有这样的整数,但整数加法仍会导致溢出。 也很容易验证除以0会导致错误,并且不返回Infinity
。
对象
return {};
{"response": []}
停什么 我们返回一个对象并得到一个数组 ? 是的,是的。 在VKScript中,数组和对象用相同的类型表示,特别是,一个空对象和一个空数组是相同的。 在这种情况下,对象的length
属性起作用并返回属性的数量。
有趣的是,如果在对象上调用列表方法,它们的行为如何?
return {a:1, b:2, c:3}.pop();
3
pop
方法返回最后声明的属性,但这是合乎逻辑的。 更改属性的顺序:
return {b:1, c:2, a:3}.pop();
3
显然,VKScript中的对象会记住分配属性的顺序。 让我们尝试使用数字属性:
return {'2':1,'1':2,'0':3}.pop();
3
现在,让我们看一下推送的工作原理:
var a = {'2':'a','1':'b','x':'c'}; a.push('d'); return a;
{"1": "b", "2": "a", "3": "d", "x": "c"};
如您所见,push方法对数字键进行排序,并在最后一个数字键之后添加一个新值。 在这种情况下不填充“孔”。
现在尝试结合这两种方法:
var a = {'2':'a','1':'b','x':'c'}; a.push(a.pop()); return a;
{"1": "b", "2": "a", "3": "c", "x": "c"};
如我们所见,该元素尚未从数组中删除。 但是,如果我们将push
和pop
放在不同的行中,该错误将消失。 我们需要更深入!
对象存储
var x = {}; var y = x; xy = 'z'; return y;
{"response": []}
事实证明,与JavaScript不同,VKScript中的对象是按值存储的。 现在我们看到了字符串a.push(a.pop());
的奇怪行为a.push(a.pop());
-显然,数组的旧值保存在堆栈中,以后从那里获取。
但是,如果方法修改了该数据,该如何存储在对象中呢? 显然,调用该方法时的“额外”指令是专门为将更改写回到对象而设计的。
数组方法
结论
VKScript不是JavaScript。 与JavaScript不同,JavaScript中的对象是按值存储的,而不是按引用存储的,它们具有完全不同的语义。 但是,将VKScript用于预期目的时,差异并不明显。
PS运算符语义
提到了通过+
组合对象的注释。 在这方面,我决定添加有关操作员工作的信息。
对于数字操作(位除外),如果操作数为int
和double
,则将int
double
为double
。 如果两个操作数均为int
,则对有符号的32位整数执行操作(带有溢出)。