冷门语法的深度解读:从oxc的pr出发,聊聊js-label标记语句

1,529 阅读6分钟

前言

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语句而非循环。

image.png

以上代码中,内外名称都是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。

简单总结下:只考虑祖先节点,即为从根节点到当前节点路径上的节点。对于兄弟节点,是忽略的。

因此,下面的代码是可以的。

image.png

假如想搞复杂一些,如下代码同样符合语法。

image.png

编译侧校验

那么编译工具如何对此校验呢?

我们考虑如下代码,并且对深度进行注释标注

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…

pr: github.com/oxc-project…

我的github: github.com/heygsc

欢迎关注~