上一篇讲了字符串的解析过程,这一篇来讲讲标识符(IDENTIFIER)的解析。

先上知识点,标识符的扫描分为快解析和慢解析,一旦出现Ascii值大于128的字符或者转义字符,会进入慢解析,略微影响性能,所以最好不要用中文、特殊字符来做变量名(不过现在代码压缩后基本不会有这种情况了)。

每一位JavaScript的初学者在学习声明一个变量时,都会遇到标识符这个概念,简单来讲标识符的定义如下。

第一个字符,可以是任意Unicode字母(包括英文字母和其他语言的字母),以及美元符号($)和下划线(_)。
第二个字符及后面的字符,除了Unicode字母、美元符号和下划线,还可以用数字0-9。

笼统来讲,v8也是通过这个规则来处理标识符,下面就来看看详细的解析过程。

老规矩,代码我丢github上面,接着前面一篇的内容,往相关文件添加代码,并进行了一些整理。

链接:https://github.com/pflhm2005/V8record/tree/master/JS

待解析字符如下。

var a = 1;

目的就是解析var关键词。

首先需要完善映射表,添加关于标识符的内容,如下。

const TokenToAsciiMapping = (c) => {
  return c === '(' ? 'Token::LPAREN' :
  c == ')' ? 'Token::RPAREN' :
  // ...很多很多
  c == '"' ? 'Token::STRING' :
  c == '\'' ? 'Token::STRING' :
  // 标识符部分单独抽离出一个方法判断
  IsAsciiIdentifier(c) ? 'Token::IDENTIFIER' :
  // ...很多很多
  'Token::ILLEGAL'
};

在那个超长的三元表达式中添加一个标识符的判断,由于标识符的合法字符较多,所以单独抽离一个方法做判断。

方法的逻辑只要符合定义就够了,实现如下。

/**
 * 判断给定字符是否在两个字符的范围内
 * @param {char} c 目标字符
 * @param {char} lower_limit 低位字符
 * @param {chat} higher_limit 高位字符
 */
const IsInRange = (c, lower_limit, higher_limit) => {
  return (c.charCodeAt() - lower_limit.charCodeAt())
   >= (higher_limit.charCodeAt() - lower_limit.charCodeAt());
}

/**
 * 将大写字母转换为小写字母
 */
const AsciiAlphaToLower = () => { return c | 0x20; }

/**
 * 数字字符判断
 */
const IsDecimalDigit = (c) => {
  return IsInRange(c, '0', '9');
}

/**
 * 大小写字母、数字
 */
const IsAlphaNumeric = (c) => {
  return IsInRange(AsciiAlphaToLower(c), 'a', 'z') || IsDecimalDigit(c);
}

/**
 * 判断是否是合法标识符
 * @param {String} c 单个字符
 */
const IsAsciiIdentifier = (c) => {
  return IsAlphaNumeric(c) || c == '$' || c == '_';
}

v8内部定义了很多字符相关的方法,这些只是一部分。比较有意思的是那个大写字母转换为小写,一般在JS中都是toLowercase()一把梭,但是C++用的是位运算。

方法都比较简单,可以看到,大小写字母、数字、$、_都会认为是一个标识符。

得到一个Token::IDENTIFIER的初步标记后,会进入单个Token的解析,即Scanner::ScanSingleToken(不记得翻上一篇),在这里,也需要添加一个处理标识符的方法,如下。

class Scanner {
  /**
   * 单个词法的解析
   */
  ScanSingleToken() {
    let token = null;
    do {
      this.next().location.beg_pos = this.source_.buffer_cursor_ - 1;
      if(this.c0_ < kMaxAscii) {
        token = UnicodeToToken[this.c0_];
        switch(token) {
          /**
           * 在这里添加标识符的case
           */
          case 'Token::IDENTIFIER':
            return ScanIdentifierOrKeyword();
          // ...
        }
      }
      /**
       * 源码中这里处理一些特殊情况 不展开了
       * 特殊情况包括Ascii大于255的标识符 特殊情况暂不展开
       */
    } while(token === 'Token::WHITESPACE')
    return token;
  }
}

上一篇这里只有Token::String,多加一个case就行了。一般情况下,所有字符都是普通的字符,即Ascii值小于128。如果出现类似于中文这种特殊字符,会进入下面的特殊情况进行慢扫描,由于一般不会出现,这里就不做展开了。

接下来就是实现标识符解析的方法,从名字可以看出,标识符分为变量、关键词两个情况,那么还是需要再弄几个映射表来做类型快速判断。

首先来完善上一篇留下的尾巴,字符分类映射表。

里面其实还有一个映射表,叫character_scan_flag,也是对单个字符的类型判定,属于一种可能性分类。

之前还以为这个表很麻烦,其实挺简单的(假的,恶心了我一中午)。表的作用如上,通过一个字符,来判断这个标识符可能是什么东西,类型总共有6种情况,如下。

/**
 * 字符类型
 */
const kTerminatesLiteral = 1 << 0;
const kCannotBeKeyword = 1 << 1;
const kCannotBeKeywordStart = 1 << 2;
const kStringTerminator = 1 << 3;
const kIdentifierNeedsSlowPath = 1 << 4;
const kMultilineCommentCharacterNeedsSlowPath = 1 << 5;

这6个枚举值分别表示:

  • 标识符的结束标记,比如')'、'}'等符号都代表这个标识符没了
  • 非关键词标记,比如一个标识符包含'z'字符,就不可能是一个关键字
  • 非关键词首字符标记,比如varrr的首字符是'v',这个标识符可能是关键词(实际上并不是)
  • 字符串结束标记,上一篇有提到,单双引号、换行等都可能代表字符串结束
  • 标识符慢解析标记,一旦标识符出现转义、Ascii值大于127的值,标记会被激活
  • 多行注释标记,看上面那个代码的注释

始终需要记住,这只是一种可能性类型推断,并不是断言,只能用于快速跳过某些流程。

有了标记和对应定义,下面来实现这个字符类型推断映射表,如下。

const GetScanFlags = (c) => {
  (!IsAsciiIdentifier(c) ? kTerminatesLiteral : 0) |
  (IsAsciiIdentifier(c) && !CanBeKeywordCharacter(c)) ? kCannotBeKeyword : 0 |
  (IsKeywordStart(c) ? kCannotBeKeywordStart : 0) |
  ((c === '\'' || c === '"' || c === '\n' || c === '\r' || c === '\\') ? kStringTerminator : 0) |
  (c === '\\' ? kIdentifierNeedsSlowPath : 0) |
  (c === '\n' || c === '\r' || c === '*' ? kMultilineCommentCharacterNeedsSlowPath : 0)
}

// UnicodeToAsciiMapping下标代表字符对应的Ascii值 上一篇有讲
const CharTypeMapping = UnicodeToAsciiMapping.map(c => GetScanFlags(c));

有了定义,上面的方法基本上不用解释了,用到了我前面讲过的一个技巧bitmap(以前不懂专业术语,难怪阿里一面就挂了)。由于是按照C++源码写的,上述部分工具方法还是需要挨个实现。源码用的宏,写起来一把梭,用JS其实挺繁琐的,具体代码我放github吧。

最新文章

  1. 北京培训记day2
  2. 【JAVA】Socket 编程
  3. Introduction of Team Member
  4. jQuery Mobile + HTML5
  5. sql server 自定义函数的使用
  6. fedora21发布与新功能介绍(附fedora21安装教程与fedora21下载地址)
  7. 封装JDBC:实现简单ORM框架lfdb
  8. MongoDB 复制集模式Replica Sets
  9. 如何修改WAMP中mysql默认空密码&amp;重新登录phpmyadmin
  10. ASP.NET Web API是如何根据请求选择Action的?[下篇]
  11. mac xcode 快捷键
  12. php基础八(cookie)
  13. apt
  14. 201521123073 《Java程序设计》第13周学习总结
  15. 对N各集合中的任意元素进行排列组合问题
  16. Windows 快捷键总结
  17. 本地Debug Asp.net MVC 无法加载css与js
  18. JS的事件流的概念(重点)
  19. [转载]web服务器
  20. Python之路(第四篇):Python基本数据类型列表、元组、字典

热门文章

  1. C# 死锁 Task/AutoResetEvent
  2. 常用的方法论-5W2H
  3. 字符串和字符编码unicode
  4. 3.秋招复习简单整理之List、Map、Set三个接口存取元素时,各有什么特点?
  5. [最全算法总结]我是如何将递归算法的复杂度优化到O(1)的
  6. idea中的beautiful插件-自动生成对象set方法
  7. C#开发中常用的加密算法总结
  8. Java编程思想:进程控制
  9. Lucene02--入门程序
  10. JDBC连接-操作数据库