JSON.parse原理之数组解析

2,099 阅读2分钟

JSON.parse是浏览器内置的API,像v8内部就是用c++实现的。JSON.parse在前端是个很常用的工具,用来将字符串转为json格式的数据。今天先来看个简单版的数组解析。其中使用到了编译原理中状态机的概念,没听过不要紧,看图就行。

数组类型的状态机

image.png

这就是数组类型的状态机图,只要能在这个图中从开始走到结束就是正确的数组类型。从一个简单的例子来看,[1,2,3],在状态图中描述的路径就是: 开始 -> [ -> num -> , -> num -> , -> num -> ] -> 结束。这是数组只有一层的情况。

可以看到在[上面还有一个自循环的箭头,表明[的下一个字符还是[]同理。这就是嵌套数组的情况,嵌套数组还可以通过, -> [的路径实现。在这个状态机里为了简单,没有标明[]的数量,实际情况下只有[]的数量和位置满足特定的要求,才会是一个合格的嵌套数组。

在这里我们先考虑输入是合理的情况。以[1, [2, [3]], 4]嵌套数组为例,它在状态图中的路径是: 开始 -> [ -> num -> , -> [ -> num -> [ -> num -> ] -> ] -> , -> num -> ] -> 结束

实现数组解析

首先是一个基本架构

function parseArrays(str){
    let i = 0;
    // TODO
}

初始化i作为遍历字符串的下标,当i到达字符串结束位置时,就结束遍历。

在状态图中没有出现空格的状态,但是其实每次都需要处理前后的空格。接下来就写几个函数分别处理空格,,num数字。

function skipWhiteSpace(){
    // 遇到空格,直接将i后移一个位置
    while(str[i] === ' ') ++i;
}
function eatComma(){
    // 首先跳过逗号前面的空格
    skipWhiteSpace();
    // 遇到空格,将i后移
    if(str[i] === ','){
        ++i;
        // 跳过逗号后面的空格
        skipWhiteSpace();
    }  
}
function parseNum(){
    // 这里只处理正整数,记录数字的起始位置
    let first = i;
    while(str[i] >= '0' && str[i] <= '9') ++i;
    // 用parseInt处理从起始位置到结束位置的num区域
    return parseInt(str.substr(first, i - first));
}

接下来就是对数组的一个处理了,在遇到嵌套数组时,使用递归调用的方式处理。在处理完数组后,需要++i移动到下一个位置,就是为了在处理完嵌套的数组后,回到原来的数组处理逻辑。

function parseArray(){
    // 跳过开始的空格
    skipWhiteSpace();
    // res存储结果
    let res = [];
    // 以`[`为开始
    if(str[i] === '['){
        i++;
        let initial = true;
        while(str[i] !== ']'){
            // 开始的时候,不能有逗号,之后处理逗号
            if(!initial){
                eatComma();
            }
            // 遍历过程中又遇到`[`,说明是嵌套数组,用递归的方式处理
            if(str[i] === '['){
                let subArr = parseArray();
                res.push(subArr);
            }else{
                // 处理遇到的数字
                let value = parseNum();
                // 数字有效时,存入结果中
                if(value) res.push(value);
                initial = false;
            }
        }
    }
    // 移动到`]`的下一个位置,跳回嵌套数组上一层的处理逻辑
    ++i;
    return res;
}

完整的实现代码如下:

let str = '[1,[2, [[3], 4]],5,6]'

function parseArrays(str){
    let i = 0;
    function skipWhiteSpace(){
        // 遇到空格,直接将i后移一个位置
        while(str[i] === ' ') ++i;
    }
    function eatComma(){
        // 首先跳过逗号前面的空格
        skipWhiteSpace();
        // 遇到空格,将i后移
        if(str[i] === ','){
            ++i;
            // 跳过逗号后面的空格
            skipWhiteSpace();
        }  
    }
    function parseNum(){
        // 这里只处理正整数,记录数字的起始位置
        let first = i;
        while(str[i] >= '0' && str[i] <= '9') ++i;
        // 用parseInt处理从起始位置到结束位置的num区域
        return parseInt(str.substr(first, i - first));
    }
    function parseArray(){
        // 跳过开始的空格
        skipWhiteSpace();
        // res存储结果
        let res = [];
        // 以`[`为开始
        if(str[i] === '['){
            i++;
            let initial = true;
            while(str[i] !== ']'){
                // 开始的时候,不能有逗号,之后处理逗号
                if(!initial){
                    eatComma();
                }
                // 遍历过程中又遇到`[`,说明是嵌套数组,用递归的方式处理
                if(str[i] === '['){
                    let subArr = parseArray();
                    res.push(subArr);
                }else{
                    // 处理遇到的数字
                    let value = parseNum();
                    // 数字有效时,存入结果中
                    if(value) res.push(value);
                    initial = false;
                }
            }
        }
        // 移动到`]`的下一个位置,跳回嵌套数组上一层的处理逻辑
        ++i;
        return res;
    }
    // 直接调用并返回parseArray的结果
    return parseArray();
}

let r = parseArrays(str);

上面的程序可以在没有异常的情况下跑通,在之后的完整的JSON.parse原理中也将处理异常。

参考