最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • AST 抽象语法树

    正文概述 掘金(亦黑迷失)   2021-06-20   398

    这是我参与更文挑战的第7天,活动详情查看: 更文挑战

    简介

    抽象语法树(Abstract Syntax Tree)本质上是一个 js 对象
    抽象语法树和虚拟节点的关系,如下图:

    AST 抽象语法树

    相关算法储备

    指针思想

    指针就是下标位置
    相关练习题:寻找字符串中连续重复次数最多的字符

    // 寻找字符串中连续重复次数最多的字符
    const str = 'aaaaaaaabbbbbbbbbbbbbbbbbcccccccccdddddd'
    // start 和 end: 指针
    let start = 0, end = 1, maxChar = str[0], maxCharLength = 0
    // 当 start 指针在 str 长度范围内时进行循环
    while (start < str.length){
      // 如果两个指针指向的字符不一样了,说明不是连续重复的字符了
      if (str[start] !== str[end]) {
        // 如果指针之差大于之前存储的最大的连续数
        if (end - start > maxCharLength) {
          maxCharLength = end - start
          maxChar = str[start]
        }
        // 让 start 指针直接追上 end 指针
        start = end
      }
      // 每次循环 end 指针都向后移动一位
      end++
    }
    console.log(maxChar, maxCharLength) // b 17
    

    递归

    凡是遇到“规则重复”,就要想到递归

    斐波那契数列

    相关练习题:用递归的方法输出斐波那契数列前 10 项

    function fn(n) {
      console.count(n)
      return n === 0 || n === 1 ? 1 : fn(n-1) + fn(n-2)
    }
    
    for (let i = 0; i < 10; i++) {
      console.log(fn(i))
    }
    

    像上面这种解法,会有大量的重复执行,比如 fn(9) 的时候,会去执行 fn(8)fn(7),而执行 fn(8) 就会再执行一遍 fn(7)。为了避免这种重复的计算,我们可以用一个对象来缓存(cache)已经执行过的函数计算。如下:

    // 设置一个缓存对象,用于存储 fn(n) 的值
    let cache = {}
    function fn(n) {
      console.count(n)
      // 如果 cache 有 n 属性
      if (cache.hasOwnProperty(n)) {
        return cache[n]
      } else { // cache 没有 n 属性,说明是第一次计算
        const v = n === 0 || n === 1 ? 1 : fn(n-1) + fn(n-2)
        cache[n] = v
        return v
      }
    }
    for (let i = 0; i < 10; i++) {
      console.log(fn(i))
    }
    

    形式转换

    练习题:将数组 [1, 2, 3, [4, 5, [6, 7]], 8] 转为下图所示的对象格式

    AST 抽象语法树
    此题有 2 种解法
    1. 递归数组
    这种方法只有在遇到传给 convert 的参数为数组时,才递归

    const arr = [1, 2, 3, [4, 5]]
    function convert(arr) {
      let convertArr = []
      for (let i = 0; i < arr.length; i++) {
        if (typeof arr[i] === 'number') {
          convertArr.push({ 'value': arr[i] })
        } else if (Array.isArray(arr[i])) {
          convertArr.push({ 'children': convert(arr[i]) })
        }
      }
      return convertArr
    }
    const res = convert(arr)
    

    2. 递归数组的子元素
    这里巧妙的运用了 map 方法的特点,从而传递给 convert2 的参数无论是数组还是数字,都递归

    const arr = [1, 2, 3, [4, 5]]
    function convert2(item) {
      if (typeof item === 'number') {
        return { 'value': item }
      } else if (Array.isArray(item)) {
        return { 'children': item.map(_item => convert2(_item)) }
      }
    }
    
    const res = convert2(arr)
    

    练习题:将字符串 3[1[a]2[b]] 转换成 abbabbabb
    这里就用到栈的思想,准备两个栈,一个存放数字,一个存放临时字符串,用一个指针遍历 3[1[a]2[b]],

    • 当指针指向的为数字时,就把数字压入数字栈中
    • 当指针指向的为[时,就把一个空字符串压入字符串栈中
    • 当指针指向的为字母时,就把字符串栈中栈顶的这一项改为这个字母
    • 当指针指向的为]时,就把数字弹栈,字符串中栈顶的这项重复刚刚这个弹出的数字次数,弹栈,然后拼接到新栈顶

    图示如下(这里没考虑数字或字母重复的情况,代码里会考虑进去)

    AST 抽象语法树
    代码实现

    const str = '3[2[9abc]11[d]]'
    
    // 指针
    let i = 0
    // 字符串从指针位置开始直至结束的部分
    let restStr = str
    // 存放数字的栈
    const stackNum = []
    // 存放字符串的栈
    const stackStr = []
    
    function smartRepeat(templateStr) {
      // 这里用 while 而不用 for 循环,因为 i 不一定每次都是 +1
      while (i < str.length - 1) { 
        /* 
        -1 是因为 str 最后一个必为 ],如果不 -1,那么本例中当指针指到最后一个 ] 时,
        将对数字栈的栈顶,也是最后一个元素 3 进行出栈,
        然后是字符串栈的栈顶,也是最后一个元素 abcabcddddddddddd 进行出栈,然后重复 3 遍拼接到字符串栈的新栈顶,
        可是此时字符串栈已经没有元素了,新栈顶将是 undefined 
        */
        restStr = str.substring(i)
        // 如果是 数字 后面紧跟 [ 开头的字符串
        if (/^(\d+)\[/.test(restStr)) {
          // 捕获数字部分
          const nums = restStr.match(/^(\d+)\[/)[1]
          // 把数字压入数字栈
          stackNum.push(nums)
          // 把空字符串压入字符串栈
          stackStr.push('')
          // 指针跳过相应的长度,+1 是因为把 ] 一起跳过了  
          i += nums.length + 1
        } else if (/^(\w+)\]/.test(restStr)) { // 如果是 字母 后面紧跟 ] 开头的字符串
          // 捕获字母部分
          const str = restStr.match(/^(\w+)\]/)[1]
          // 将字符串栈的栈顶的那一项赋值为捕获的字母
          stackStr[stackStr.length - 1] = str
          // 直接跳过字母的长度
          i += str.length
        } else if (restStr[0] === ']') {
          // 对数字栈进行出栈操作
          const popNum = stackNum.pop()
          // 对字符串栈进行出栈
          const popStr = stackStr.pop()
          // 字符串拼接
          stackStr[stackStr.length - 1] += popStr.repeat(popNum)
          i++ 
        }
      }
      // while 循环结束,此时数字栈和字符串栈各自剩下最后一个元素,将 字符串 重复 数字 遍返回
      return stackStr[0].repeat(stackNum[0])
    }
    const result = smartRepeat(str)
    console.log(result) // 9abc9abcddddddddddd9abc9abcddddddddddd9abc9abcddddddddddd
    

    Tips:repeat是es6的字符串方法,构造并返回一个新字符串,该字符串包含被连接在一起的指定数量的字符串的副本。如果repeat的参数是字符串,则会先转换成数字。

    手写实现 AST

    原理

    首先注意一点,平时在 .vue 文件里写在 template 里的看似 dom 的内容,事实上会经由 vue-loader 的解析,作为字符串提取处理。实现 AST 的原理根本上就是把一段字符串通过指针逐个遍历,根据不同情况进行不同的处理,进行一些栈操作,类似上文中栈里练习题。
    比如,想要将如下代码转成 AST

    <div>
      <h3>范特西</h3>
      <ul>
        <li>七里香</li>
      </ul> 
    </div>
    

    转换目标(AST)

    {
      tag: "div", 
      children: [
        {
          tag: "h3", 
          children: [ { text: "范特西", type: 3 }], 
          type: 1,
        },
        {
          tag: "ul", 
          children: [
            {
              tag: "li", 
              children: [{ text: "七里香", type: 3 }], 
              type: 1,
            }
          ], 
          type: 1,
        }
      ], 
      type: 1
    }
    

    我们可以准备两个栈和一个用于遍历模板字符串的指针:

    • 指针遇到标签则往一个栈(标签栈)中加入该标签名,另一个栈(数组栈)中加入一个空数组(代码里为了方便事实上是加入一个对象 { tag: startTag, children: [] })
    • 指针遇到文字则将数组栈中的栈顶的数组内容改为文字
    • 指针遇到闭合标签则将标签栈和数组栈都进行出栈操作(数组栈出栈的内容就是标签栈出栈的标签的内容),然后将出栈的这两个元素组合下,拼接到数组栈的新栈顶的那个数组里。

    动图示意如下 AST 抽象语法树

    代码

    // index.js
    import parse from './parse.js'
    const templateStr = `<div>
      <h3 id="legend" class="jay song">范特西</h3>
      <ul>
        <li>七里香</li>
      </ul> 
    </div>`
    
    const ast = parse(templateStr)
    console.log(ast)
    
    // parse.js
    import parseAttrs from './parseAttrs.js'
    
    export default function(templateStr) {
      // 准备一个指针
      let i = 0
      // 准备两个栈
      // 初始添加元素 { children: [] } 是因为如果不加, stackContent 在遇到最后一个封闭标签进行弹栈后,stackContent 里就没有元素了,也没有 .children 可以去 push 了
      const stackTag = [], stackContent = [{ children: [] }] 
      // 指针所指位置为开头的剩余字符串
      let restTemplateStr = templateStr
      // 识别开始标签的正则
      const regExpStart = /^<([a-z]+[1-6]?)(\s?[^>]*)>/
    
     while (i < templateStr.length - 1) {
      restTemplateStr = templateStr.substring(i)
      // 遇到开始标签
      if (regExpStart.test(restTemplateStr)) {
        const startTag = restTemplateStr.match(regExpStart)[1] // 标签
        const attrsStr = restTemplateStr.match(regExpStart)[2] // 属性
        // 标签栈进行压栈
        stackTag.push(startTag)
        // 内容栈进行压栈
        stackContent.push({
          tag: startTag,
          attrs: parseAttrs(attrsStr),
          type: 1,
          children: []
        })
        i += startTag.length + attrsStr.length  + 2 // +2 是因为还要算上 < 和 >
      } else if (/^<\/[a-z]+[1-6]?>/.test(restTemplateStr)) { // 遇到结束标签
        const endTag = restTemplateStr.match(/^<\/([a-z]+[1-6]?)>/)[1]
        // 结束标签应该与标签栈的栈顶标签一致
        if (endTag === stackTag[stackTag.length -1]) {
          // 两个栈都进行弹栈
          stackTag.pop()
          const popContent = stackContent.pop()
          stackContent[stackContent.length - 1].children.push(popContent)
          i += endTag.length + 3 // +3 是因为还要算上 </ 和 >
        } else {
          throw Error('标签' + stackTag[stackTag.length -1] + '没有闭合')
        }
      } else if (/^[^<]+<\/[a-z]+[1-6]?>/.test(restTemplateStr)) { // 遇到内容
        const wordStr = restTemplateStr.match(/^([^<]+)<\/[a-z]+[1-6]?>/)[1] // 捕获结束标签 </> 之前的内容,并且不能包括开始标签 <>
        if (!/^\s+$/.test(wordStr)) { // 如果捕获的内容不为空
          // 将内容栈栈顶元素进行赋值
          stackContent[stackContent.length - 1].children.push({
            text: wordStr,
            type: 3
          })
        }
        i += wordStr.length
      } else {
        i++
      }
     }
     // 因为定义 stackContent 的时候就默认添加了一项元素 { children: [] },现在只要返回 children 的第一项就行 
     return stackContent[0].children[0]
    }
    

    为了处理标签内可能的属性,注意,我们截取每个属性的标准不是简单的判断空格,因为属性里可能有多个值,他们之间可能有空格,所以判断依据是不在双引号内的空格

    // parseAttrs.js
    export default function(attrsStr) {
      const attrsStrTrim = attrsStr.trim() // 去空格
      if (attrsStrTrim) {
        let point = 0 // 断点
        let isYinhao = false // 是否是引号
        let result = [] // 结果数组
        for (let index = 0; index < attrsStrTrim.length; index++) {
          if (attrsStrTrim[index] === '"') isYinhao = !isYinhao
          // 遇到空格且不在双引号内,就截取从 point 到此的字符串
          if (!isYinhao && /\s/.test(attrsStrTrim[index])) {
            const attrs = attrsStrTrim.substring(point, index)
            result.push(attrs)
            point = index
          }
        }
        result.push(attrsStrTrim.substring(point + 1)) // 最后一个属性是没有通过 for 循环得到的,所以要专门加上,+1 是为了去除开始的空格
        // ["id="legend"", "class="jay song""]
        result = result.map(item => {
          // 根据等号拆分
          const itemMatch = item.match(/(.+)="(.+)"/)
          return {
            name: itemMatch[1],
            value: itemMatch[2]
          }
        })
        return result
      } else {
        return []
      }
    }
    

    至此,本次分享的主要内容已经结束~

    One More Thing

    这里对上文代码中用到的 console.count()hasOwnProperty() 做点补充说明

    console.count()

    首先,该特性是非标准的,请尽量不要在生产环境中使用它!
    输出 count() 被调用的次数,接受一个可选参数 label,每次调用,如果标签一样,则对应的计数数字会增加 1,如果不一样则重新开始计数

    hasOwnProperty()

    用来检测一个对象是否含有特定的自身属性;
    in 运算符不同,hasOwnProperty 会忽略掉那些从原型链上继承到的属性,
    也就是上面的代码 if (cache.hasOwnProperty(n)) 其实也可以写成 if (n in cache)

    AST 抽象语法树
    AST 抽象语法树


    起源地下载网 » AST 抽象语法树

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元