前言
js中,有一个不是很热门的语法:label标记语句。
label很容易让大家联想到经典的goto语句。关于goto呢,是有些争议的,这里我们不做过多的讨论。所以单纯的常规意义/用法/实践在此处不作为重点。
既然label已经存在,这里主要聊聊label本身,编译相关的一些好玩的事情。
注意:此语法与html的label标签元素<label></label>
,为不太相关的两种东西,请勿混淆。
用法
developer.mozilla.org/zh-CN/docs/…
这里是mdn链接,有相关用法介绍。如以下例子可见,标记常用于多层嵌套中,结合continue/break,为了找到我们的目标层。
// 第一个 for 语句被标记为“loop1”
loop1: for (let i = 0; i < 3; i++) {
// 第二个 for 语句被标记为“loop2”
loop2: for (let j = 0; j < 3; j++) {
if (i === 1 && j === 1) {
continue loop1;
}
console.log(`i = ${i}, j = ${j}`);
}
}
// 输出:
// i = 0, j = 0
// i = 0, j = 1
// i = 0, j = 2
// i = 1, j = 0
// i = 2, j = 0
// i = 2, j = 1
// i = 2, j = 2
那假如这多层中间,有重复的标记,js是否知道我们在哪层呢?
答案是不行的,所以会报错。
参考如下错误代码,注:一般标记后面是循环,但是语法要求是语句,而非声明即可。所以为了方便看,使用符合语法的console语句而非循环。
以上代码中,内外名称都是Test,有重复。js无法得知我们需要的Test是哪个,这里会报错。
进一步思考
既然如上所示的根节点子节点,标记不可以重复。那么兄弟节点是否可以重复呢?
如果你在Chrome Devtools之类的地方试了下,会发现:可以的。
请看如下代码。
// 外层loop1
loop1: for (let i = 1; i <= 3; i++) {
// 内层loop2
loop2: for (let j = 1; j <= 3; j++) {
if (i === 2 && j === 1) {
continue loop2;
}
console.log(`first : i = ${i}, j = ${j}`);
}
// 内层第二个loop2
loop2: for (let j = 5; j <= 7; j++) {
if (i === 2 && j === 6) {
continue loop2;
}
console.log(`second : i = ${i}, j = ${j}`);
}
}
// first : i = 1, j = 1
// first : i = 1, j = 2
// first : i = 1, j = 3
// second : i = 1, j = 5
// second : i = 1, j = 6
// second : i = 1, j = 7
// first : i = 2, j = 2
// first : i = 2, j = 3
// second : i = 2, j = 5
// second : i = 2, j = 7
// first : i = 3, j = 1
// first : i = 3, j = 2
// first : i = 3, j = 3
// second : i = 3, j = 5
// second : i = 3, j = 6
// second : i = 3, j = 7
这里通过空行把注释分为3个部分。
第1部分和第3部分的if语句条件不符合,所以没有使用到label。因此i=1或3时,正常遍历,无事发生。
我们下面看第2部分。
第1个loop2就是跳过了j=1,直接走到j=2和j=3。
而且这里的标记,效果很好,js没有去寻找第2个loop2,只寻找了这第1个loop2,即当前语句的祖先节点
,而忽略了兄弟节点
。
第2个loop2同样没有去理会第1个loop2。continue跳过j=6,直接j=7。
简单总结下:只考虑祖先节点
,即为从根节点到当前节点路径上的节点
。对于兄弟节点
,是忽略的。
因此,下面的代码是可以的。
假如想搞复杂一些,如下代码同样符合语法。
编译侧校验
那么编译工具如何对此校验呢?
我们考虑如下代码,并且对深度进行注释标注
Parent: { // depth:1
Child1: { // depth:2
Child2: console.log("1"); // depth:3
}
Child1: { // depth:2
Child2: console.log("2"); // depth:3
}
}
如上所示,5个节点,标注5个深度。我们可以得到这样一个数组[1,2,3,2,3]
。
根据我们前面的结论,我们只需要校验根节点到当前节点路径上
,是否有重复
的label标记名称
。
遍历这个数组,到第一个3的时候,[1,2,3]
就是根节点到第一个Child2的路径。
继续往下走,到第2个Child1,数组变为[1,2,3,2]
。此时的数组中,就不是单纯的根节点到当前节点路径了,包含不相关节点,我们需要去更新这个数组。
如何更新呢?
考虑到根节点到当前节点的深度n,数组必定是[1,2,3,...n]
形式的。即为新进来的一项
,必定比目前的最后一项大1
,这样才是根节点到当前节点的路径
。
那么当把新一项放进来,除了比最后一项大1,还有什么可能性呢?
比最后一项大2及以上是不可能的,这意味着跳过了些节点,因为深度不会突然增加2及以上。
所以除了比最后一项大1,只有2种可能:1.等于最后一项 2.小于最后一项。下面讨论这2种情况。
1.等于最后一项
这意味着和上一项深度相同。即为兄弟节点。
我们前面提到过,兄弟节点允许重名,所以需要保留新进来的一项,去除本来的最后一项。
因为校验对于当前层,是忽略的,不需要把兄弟节点加进来。只要保留当前节点,以备后面的字节点使用。
2.小于最后一项
关于这个可能,现在可以回过头来继续看我们的例子[1,2,3,2]
。
这4个数中,第4个数的父节点是根节点,即新的数组应为[1,2]
,删去第2个和第3个数。
怎么得知需要删除哪个呢?
由于在新加进来一项之前,现有的数组是符合要求的,即为[1,2,3,...n]
形式。
我们只需要找到,与新加项相同的那一项
,删除那一项
以及后续所有项
。然后把新加一项放到最后,这就是我们要的。
这个思路可以解决所有小于最后一项的情况。
oxc的pr
由于rust的性能出众等原因,出现了一些rust编写的前端工具链。oxc就是如此:oxc.rs
oxc的众多功能之一,就是可以通过对ast的分析等等,来校验我们的label是否符合要求。
我当时发现了oxc的github有一个bug issue,大意是label的重复名称校验,对兄弟节点以及其他情况是有误的。比如对重名兄弟节点的报错。我分析之后进行了pr,后被merge。
下面是修复校验逻辑中的核心代码,对rust不熟没关系,我做了些注释
let mut defined = FxHashMap::default(); //定义需要维护的HashMap,用来判断是否重复
let mut increase_depth = vec![]; //定义需要维护的数组
for labeled in labels {
increase_depth.push(labeled);
//由于HashMap的元素只能一个一个删除等原因,这里没有截段落删除,而是进行遍历
while increase_depth.len() > 2
&& increase_depth.iter().rev().nth(1).unwrap().depth //倒数第二项
>= increase_depth.iter().next_back().unwrap().depth //最后一项
{
defined.remove(increase_depth[increase_depth.len() - 2].name); //维护HashMap
increase_depth.remove(increase_depth.len() - 2); //维护数组
}
if let Some(span) = defined.get(labeled.name) {
//如果找到HashMap中有重复的,则进行报错提示
ctx.error(label_redeclaration(labeled.name, *span, labeled.span));
} else {
defined.insert(labeled.name, labeled.span);
}
}
附录
issue: github.com/oxc-project…
我的github: github.com/heygsc
欢迎关注~