首頁 > 軟體

JavaScript正規表示式中g標誌詳解

2022-03-18 13:01:15

緣起

有一天在思否社群看到有個問題,大致描述如下

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 關鍵詞

進入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

IT145.com E-mail:sddin#qq.com