目录
  • 缘起
  • 解密过程
    • 搜索引擎
    • 源码层面
  • 结论

    缘起

    有一天在思否社区看到有个问题,大致描述如下

    const list = ['a', 'b', '-', 'c', 'd'];
    const reg = /[a-z]/g;
    const letters = list.filter(i => reg.test(i));
    
    // letters === ['a', 'c'];
    // 如果正则不使用`g`标志可以得到所有的字母
    // 为什么加入`g`之后就不可以了

    对问题而言,遍历中的i就是一个字符,不需要用到g。

    但是就我对正则的理解(过于浅薄)感觉上有没有g(只是全局搜索,不会匹配到就停下来)应该不影响,激发了我的好奇心。

    上面题的建议写法如下

    const reg = /[a-z]/g;
    reg.test('a'); // => true
    reg.test('a'); // => false
    reg.test('a'); // => true
    reg.test('a'); // => false
    reg.test('a'); // => true

    解密过程

    首先可以确定的表现一定是g导致的

    搜索引擎

    打开 MDN 仔细查看g标志的作用,得到结论和我的理解无二。

    我猜想应该就是g可能启用了某种缓存,又因为reg相对过滤器是全局变量,我将代码改为:

    const list = ['a', 'b', '-', 'c', 'd'];
    const letters = list.filter(i => /[a-z]/g.test(i));
    
    // letters === ['a', 'b', 'c', 'd'];

    将正则声明到每一次遍历,得到结论就是正确的,验证了我的猜想。也得到了,缓存就是正则中的某个地方

    下面我找到对应的源码来查看问题的原因

    源码层面

    由于最近在看 Rust,所以使用 Rust 编写的源码查看

    打开项目后,点击.进入 vscode 模式,command+p 搜索 regexp 关键词

    JavaScript正则表达式中g标志详解

    进入test.rs文件,command+f 搜索/g可以找到在 90 行有个last_index()的测试

    #[test]
    fn last_index() {
        let mut context = Context::default();
        let init = r#"
            var regex = /[0-9]+(\.[0-9]+)?/g;
            "#;
        // forward 的作用:更改 context,并返回结果的字符串。
        eprintln!("{}", forward(&mut context, init));
        assert_eq!(forward(&mut context, "regex.lastIndex"), "0");
        assert_eq!(forward(&mut context, "regex.test('1.0foo')"), "true");
        assert_eq!(forward(&mut context, "regex.lastIndex"), "3");
        assert_eq!(forward(&mut context, "regex.test('1.0foo')"), "false");
        assert_eq!(forward(&mut context, "regex.lastIndex"), "0");
    }

    看到了有lastIndex关键字,这里再已经大致猜到问题的原因了,g 标志存在匹配后的最后一个下标,导致出现问题。

    我们将视线移入到mod.rs文件中,搜索test

    在 631 行看到了fn test()方法

    pub(crate) fn test(
        this: &JsValue,
        args: &[JsValue],
        context: &mut Context,
    ) -> JsResult<JsValue> {
        // 1. Let R be the this value.
        // 2. If Type(R) is not Object, throw a TypeError exception.
        let this = this.as_object().ok_or_else(|| {
            context
                .construct_type_error("RegExp.prototype.test method called on incompatible value")
        })?;
    
        // 3. Let string be ? ToString(S).
        let arg_str = args
            .get(0)
            .cloned()
            .unwrap_or_default()
            .to_string(context)?;
    
        // 4. Let match be ? RegExpExec(R, string).
        let m = Self::abstract_exec(this, arg_str, context)?;
    
        // 5. If match is not null, return true; else return false.
        if m.is_some() {
            Ok(JsValue::new(true))
        } else {
            Ok(JsValue::new(false))
        }
    }

    test()方法中找到了Self::abstract_exec()方法

    pub(crate) fn abstract_exec(
        this: &JsObject,
        input: JsString,
        context: &mut Context,
    ) -> JsResult<Option<JsObject>> {
        // 1. Assert: Type(R) is Object.
        // 2. Assert: Type(S) is String.
    
        // 3. Let exec be ? Get(R, "exec").
        let exec = this.get("exec", context)?;
    
        // 4. If IsCallable(exec) is true, then
        if let Some(exec) = exec.as_callable() {
            // a. Let result be ? Call(exec, R, « S »).
            let result = exec.call(&this.clone().into(), &[input.into()], context)?;
    
            // b. If Type(result) is neither Object nor Null, throw a TypeError exception.
            if !result.is_object() && !result.is_null() {
                return context.throw_type_error("regexp exec returned neither object nor null");
            }
    
            // c. Return result.
            return Ok(result.as_object().cloned());
        }
    
        // 5. Perform ? RequireInternalSlot(R, [[RegExpMatcher]]).
        if !this.is_regexp() {
            return context.throw_type_error("RegExpExec called with invalid value");
        }
    
        // 6. Return ? RegExpBuiltinExec(R, S).
        Self::abstract_builtin_exec(this, &input, context)
    }

    又在Self::abstract_exec()方法中找到了Self::abstract_builtin_exec()方法

    pub(crate) fn abstract_builtin_exec(
    this: &JsObject,
    input: &JsString,
    context: &mut Context,
    ) -> JsResult<Option<JsObject>> {
    // 1. Assert: R is an initialized RegExp instance.
    let rx = {
    let obj = this.borrow();
    if let Some(rx) = obj.as_regexp() {
    rx.clone()
    } else {
    return context.throw_type_error("RegExpBuiltinExec called with invalid value");
    }
    };

    // 2. Assert: Type(S) is String.

    // 3. Let length be the number of code units in S.
    let length = input.encode_utf16().count();

    // 4. Let lastIndex be ℝ(? ToLength(? Get(R, "lastIndex"))).
    let mut last_index = this.get("lastIndex", context)?.to_length(context)?;

    // 5. Let flags be R.[[OriginalFlags]].
    let flags = &rx.original_flags;

    // 6. If flags contains "g", let global be true; else let global be false.
    let global = flags.contains('g');

    // 7. If flags contains "y", let sticky be true; else let sticky be false.
    let sticky = flags.contains('y');

    // 8. If global is false and sticky is false, set lastIndex to 0.
    if !global && !sticky {
    last_index = 0;
    }

    // 9. Let matcher be R.[[RegExpMatcher]].
    let matcher = &rx.matcher;

    // 10. If flags contains "u", let fullUnicode be true; else let fullUnicode be false.
    let unicode = flags.contains('u');

    // 11. Let matchSucceeded be false.
    // 12. Repeat, while matchSucceeded is false,
    let match_value = loop {
    // a. If lastIndex > length, then
    if last_index > length {
    // i. If global is true or sticky is true, then
    if global || sticky {
    // 1. Perform ? Set(R, "lastIndex", +0

    声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。