当前位置: 网站首页>小程序开发>网站制作

昌邑网站推广【昌邑办理400电话】昌邑SEO优化、昌邑微信公众号APP客户端小程序开发、昌邑网站托管、昌邑APP开发

发表日期: 2021-05-20 14:02:59 浏览次数:84

昌邑网站推广【昌邑办理400电话】昌邑SEO优化、昌邑微信公众号APP客户端小程序开发、昌邑网站托管、昌邑APP开发

网站建设.jpg

昌邑市属于潍坊市下辖的县级市,位于山东半岛西北部,潍河下游,莱州湾畔,介于北纬36°25′-37°08′,东经119°13′-119°37′之间,东与莱州市、平度市以胶莱河为界,南与高密市、安丘市毗连,西与潍坊市坊子区、寒亭区为邻,北濒莱州湾,属市域总面积1578.7平方公里。 [1-2]  截至2016年,昌邑市辖3个街道、6个镇。

昌邑市属环渤海经济圈,为国务院确定的沿海对外开放城市之一,被誉为中国丝绸之乡、华侨之乡和中国溴·盐之乡, 先后被评为中国北方绿化苗木基地、中国纺织产业基地市、中国超纤产业基地、中国中小城市综合实力百强县市 [3]  、中国最具投资潜力中小城市百强县市 [4]  等荣誉称号。 [5]  2018年10月,昌邑市入选“综合实力百强县”、 [6]  全国投资潜力百强县市、 [7]  全国绿色发展百强县市 [8]  、全国科技创新百强县市、全国新型城镇化质量百强县市 [9]  。 [10]  2019年10月8日,被评为2019年度全国综合实力百强县市、2019年度全国绿色发展百强县市、全国科技创新百强县市 [11]  。 [12-13]  2020年山东省四星级新型智慧城市建设预试点城市, [14]  全国文明城市。 [15] 



概述

稍微了解行业现状的开发者都知道,现在前端“ES6即正义”,然而浏览器的支持还是进行时。所以我们会用一个神奇的工具将ES6都给转换成目前支持比较广泛的ES5语法。对,说的就是Babel

本文不再介绍Babel是什么也不讲怎么用,这类文章很多,我也不觉得自己能写得更好。这篇文章的关注点是另一个方面,也是很多人会好奇的事情,Babel的工作原理是什么。

Babel工作的三个阶段

首先要说明的是,现在前端流行用的WebPack或其他同类工程化工具会将源文件组合起来,这部分并不是Babel完成的,是这些打包工具自己实现的,Babel的功能非常纯粹,以字符串的形式将源代码传给它,它就会返回一段新的代码字符串(以及sourcemap)。他既不会运行你的代码,也不会将多个代码打包到一起,它就是个编译器,输入语言是ES6+,编译目标语言是ES5。

在Babel官网,plugins菜单下藏着一个链接:thejameskyle/the-super-tiny-compiler。它已经解释了整个工作过程,有耐心者可以自己研究,当然也可以继续看我的文章。

Babel的编译过程跟绝大多数其他语言的编译器大致同理,分为三个阶段:

  1. 解析:将代码字符串解析成抽象语法树
  2. 变换:对抽象语法树进行变换操作
  3. 再建:根据变换后的抽象语法树再生成代码字符串

像我们在.babelrc里配置的presets和plugins都是在第2步工作的。

举个例子,首先你输入的代码如下:

if (1 > 0) {
    alert('hi');
}

经过第1步得到一个如下的对象:

{
  "type": "Program",                          // 程序根节点
  "body": [                                   // 一个数组包含所有程序的顶层语句
    {
      "type": "IfStatement",                  // 一个if语句节点
      "test": {                               // if语句的判断条件
        "type": "BinaryExpression",           // 一个双元运算表达式节点
        "operator": ">",                      // 运算表达式的运算符
        "left": {                             // 运算符左侧值
          "type": "Literal",                  // 一个常量表达式
          "value": 1                          // 常量表达式的常量值
        },
        "right": {                            // 运算符右侧值
          "type": "Literal",
          "value": 0
        }
      },
      "consequent": {                         // if语句条件满足时的执行内容
        "type": "BlockStatement",             // 用{}包围的代码块
        "body": [                             // 代码块内的语句数组
          {
            "type": "ExpressionStatement",    // 一个表达式语句节点
            "expression": {
              "type": "CallExpression",       // 一个函数调用表达式节点
              "callee": {                     // 被调用者
                "type": "Identifier",         // 一个标识符表达式节点
                "name": "alert"
              },
              "arguments": [                  // 调用参数
                {
                  "type": "Literal",
                  "value": "hi"
                }
              ]
            }
          }
        ]
      },
      "alternative": null                     // if语句条件未满足时的执行内容
    }
  ]
}

Babel实际生成的语法树还会包含更多复杂信息,这里只展示比较关键的部分,欲了解更多关于ES语言抽象语法树规范可阅读:The ESTree Spec。

用图像更简单地表达上面的结构:

第1步转换的过程中可以验证语法的正确性,同时由字符串变为对象结构后更有利于精准地分析以及进行代码结构调整。

第2步原理就很简单了,就是遍历这个对象所描述的抽象语法树,遇到哪里需要做一下改变,就直接在对象上进行操作,比如我把IfStatement给改成WhileStatement就达到了把条件判断改成循环的效果。

第3步也简单,递归遍历这颗语法树,然后生成相应的代码,大概的实现逻辑如下:

const types = {
  Program (node) {
    return node.body.map(child => generate(child));
  },
  IfStatement (node) {
    let code = `if (${generate(node.test)}) ${generate(node.consequent)}`;
    if (node.alternative) {
      code += `else ${generate(node.alternative)}`;
    }
    return code;
  },
  BlockStatement (node) {
    let code = node.body.map(child => generate(child));
    code = `{ ${code} }`;
    return code;
  },
  ......
};
function generate(node) {
  return types[node.type](node);
}
const ast = Babel.parse(...);            // 将代码解析成语法树
const generatedCode = generate(ast);     // 将语法树重新组合成代码

抽象语法树是如何产生的

第2、3步相信不用花多少篇幅大家自己都能理解,重点介绍的第一步来了。

解析这一步又分成两个步骤:

  1. 分词:将整个代码字符串分割成 语法单元 数组
  2. 语义分析:在分词结果的基础之上分析 语法单元之间的关系

我们一步步讲。

分词

首先解释一下什么是语法单元:语法单元是被解析语法当中具备实际意义的最小单元,通俗点说就是类似于自然语言中的词语。

看这句话“2020年奥运会将在东京举行”,不论词性及主谓关系等,人第一步会把这句话拆分成:2020年、奥运会、将、在、东京、举行。这就是分词:把整句话拆分成有意义的最小颗粒,这些小块不能再被拆分,否则就失去它所能表达的意义了。

那么回到代码的解析当中,JS代码有哪些语法单元呢?大致有以下这些(其他语言也许类似但通常都有区别):

  • 空白:JS中连续的空格、换行、缩进等这些如果不在字符串里,就没有任何实际逻辑意义,所以把连续的空白符直接组合在一起作为一个语法单元。
  • 注释:行注释或块注释,虽然对于人类来说有意义,但是对于计算机来说知道这是个“注释”就行了,并不关心内容,所以直接作为一个不可再拆的语法单元
  • 字符串:对于机器而言,字符串的内容只是会参与计算或展示,里面再细分的内容也是没必要分析的
  • 数字:JS语言里就有16、10、8进制以及科学表达法等数字表达语法,数字也是个具备含义的最小单元
  • 标识符:没有被引号扩起来的连续字符,可包含字母、_、$、及数字(数字不能作为开头)。标识符可能代表一个变量,或者true、false这种内置常量、也可能是if、return、function这种关键字,是哪种语义,分词阶段并不在乎,只要正确切分就好了。
  • 运算符:+、-、*、/、>、<等等
  • 括号:(…)可能表示运算优先级、也可能表示函数调用,分词阶段并不关注是哪种语义,只把“(”或“)”当做一种基本语法单元
  • 还有其他:如中括号、大括号、分号、冒号、点等等不再一一列举

分词的过过程从逻辑来讲并不难解释,但是这是个精细活,要考虑清楚所有的情况。还是以一个代码为例:

if (1 > 0) {
  alert("if \"1 > 0\"");
}

我们希望得到的分词是:

'if'     ' '       '('    '1'      ' '    '>'    ' '    ')'    ' '    '{'
'\n  '   'alert'   '('    '"if \"1 > 0\""'    ')'    ';'    '\n'   '}'

注意其中”if \”1 > 0\””是作为一个语法单元存在,没有再查分成if、1、>、0这样,而且其中的转译符会阻止字符串早结束。

这拆分过程其实没啥可取巧的,就是简单粗暴地一个字符一个字符地遍历,然后分情况讨论,整个实现方法就是顺序遍历和大量的条件判断。我用一个简单的实现来解释,在关键的地方注释,我们只考虑上面那段代码里存在的语法单元类型。

function tokenizeCode (code) {
  const tokens = [];    // 结果数组
  for (let i = 0; i < code.length; i++) {
    // 从0开始,一个字符一个字符地读取
    let currentChar = code.charAt(i);

    if (currentChar === ';') {
      // 对于这种只有一个字符的语法单元,直接加到结果当中
      tokens.push({
        type: 'sep',
        value: ';',
      });
      // 该字符已经得到解析,不需要做后续判断,直接开始下一个
      continue;
    }
    
    if (currentChar === '(' || currentChar === ')') {
      // 与 ; 类似只是语法单元类型不同
      tokens.push({
        type: 'parens',
        value: currentChar,
      });
      continue;
    }

    if (currentChar === '}' || currentChar === '{') {
      // 与 ; 类似只是语法单元类型不同
      tokens.push({
        type: 'brace',
        value: currentChar,
      });
      continue;
    }

    if (currentChar === '>' || currentChar === '<') {
      // 与 ; 类似只是语法单元类型不同
      tokens.push({
        type: 'operator',
        value: currentChar,
      });
      continue;
    }

    if (currentChar === '"' || currentChar === '\'') {
      // 引号表示一个字符传的开始
      const token = {
        type: 'string',
        value: currentChar,       // 记录这个语法单元目前的内容
      };
      tokens.push(token);

      const closer = currentChar;
      let escaped = false;        // 表示下一个字符是不是被转译的

      // 进行嵌套循环遍历,寻找字符串结尾
      for (i++; i < code.length; i++) {
        currentChar = code.charAt(i);
        // 先将当前遍历到的字符无条件加到字符串的内容当中
        token.value += currentChar;
        if (escaped) {
          // 如果当前转译状态是true,就将改为false,然后就不特殊处理这个字符
          escaped = false;
        } else if (currentChar === '\\') {
          // 如果当前字符是 \ ,将转译状态设为true,下一个字符不会被特殊处理
          escaped = true;
        } else if (currentChar === closer) {
          break;
        }
      }
      continue;
    }
    
    if (/[0-9]/.test(currentChar)) {
      // 数字是以0到9的字符开始的
      const token = {
        type: 'number',
        value: currentChar,
      };
      tokens.push(token);

      for (i++; i < code.length; i++) {
        currentChar = code.charAt(i);
        if (/[0-9\.]/.test(currentChar)) {
          // 如果遍历到的字符还是数字的一部分(0到9或小数点)
          // 这里暂不考虑会出现多个小数点以及其他进制的情况
          token.value += currentChar;
        } else {
          // 遇到不是数字的字符就退出,需要把 i 往回调,
          // 因为当前的字符并不属于数字的一部分,需要做后续解析
          i--;
          break;
        }
      }
      continue;
    }

    if (/[a-zA-Z\$\_]/.test(currentChar)) {
      // 标识符是以字母、$、_开始的
      const token = {
        type: 'identifier',
        value: currentChar,
      };
      tokens.push(token);

      // 与数字同理
      for (i++; i < code.length; i++) {
        currentChar = code.charAt(i);
        if (/[a-zA-Z0-9\$\_]/.test(currentChar)) {
          token.value += currentChar;
        } else {
          i--;
          break;
        }
      }
      continue;
    }
    
    if (/\s/.test(currentChar)) {
      // 连续的空白字符组合到一起
      const token = {
        type: 'whitespace',
        value: currentChar,
      };
      tokens.push(token);

      // 与数字同理
      for (i++; i < code.length; i++) {
        currentChar = code.charAt(i);
        if (/\s]/.test(currentChar)) {
          token.value += currentChar;
        } else {
          i--;
          break;
        }
      }
      continue;
    }

    // 还可以有更多的判断来解析其他类型的语法单元

    // 遇到其他情况就抛出异常表示无法理解遇到的字符
    throw new Error('Unexpected ' + currentChar);
  }
  return tokens;
}

const tokens = tokenizeCode(`
if (1 > 0) {
  alert("if 1 > 0");
}
`);

以上代码是我个人的实现方式,与babel实际略有不同,但主要思路一样。

执行结果如下:

[
 { type: "whitespace", value: "\n" },
 { type: "identifier", value: "if" },
 { type: "whitespace", value: " " },
 { type: "parens", value: "(" },
 { type: "number", value: "1" },
 { type: "whitespace", value: " " },
 { type: "operator", value: ">" },
 { type: "whitespace", value: " " },
 { type: "number", value: "0" },
 { type: "parens", value: ")" },
 { type: "whitespace", value: " " },
 { type: "brace", value: "{" },
 { type: "whitespace", value: "\n " },
 { type: "identifier", value: "alert" },
 { type: "parens", value: "(" },
 { type: "string", value: "\"if 1 > 0\"" },
 { type: "parens", value: ")" },
 { type: "sep", value: ";" },
 { type: "whitespace", value: "\n" },
 { type: "brace", value: "}" },
 { type: "whitespace", value: "\n" },
]

经过这一步的分词,这个数组就比摊开的字符串更方便进行下一步处理了。


昌邑网站推广昌邑办理400电话昌邑SEO优化、昌邑微信公众号APP客户端小程序开发、昌邑网站托管、昌邑APP开发

400-111-6878
服务热线
顶部

备案号: 苏ICP备11067224号

CopyRight © 2011 书生商友信息科技 All Right Reserved

24小时服务热线:400-111-6878   E-MAIL:1120768800@qq.com   QQ:1120768800

  网址: http://www.768800.com  网站建设上往建站

关键词: 网站建设| 域名邮箱| 服务器空间| 网站推广| 上往建站| 网站制作| 网站设计| 域名注册| 网络营销| 网站维护|

企业邮箱| 虚拟主机| 网络建站| 网站服务| 网页设计| 网店美工设计| 网站定制| 企业建站| 网站设计制作| 网页制作公司|

400电话办理| 书生商友软件| 葬花网| 调温纤维| 海洋馆运营维护| 北京保安公司| 殡仪馆服务| 殡葬服务| 苏州殡葬一条龙| 朝阳殡葬| 苏州殡葬服务|

预约专家

欢迎您免费咨询,请填写以下信息,我们收到后会尽快与您联系

  

服务热线:400-111-6878