上一篇讲了字符串的解析过程,这一篇来说讲标识符(IDENTIFIER)的解析。
先上知识点,标识符的扫描分为快解析和慢解析,一旦出现Ascii编码大于127的字符或者转义字符,会进入慢解析,略微影响速度,因此最好不要用中文、特殊字符来作变量名(不过如今代码压缩后基本不会有这种状况了)。
每一位JavaScript的初学者在学习声明一个变量时,都会遇到标识符这个概念,定义以下。
第一个字符,能够是任意Unicode字母(包括英文字母和其余语言的字母),以及美圆符号($)和下划线(_)。 第二个字符及后面的字符,除了Unicode字母、美圆符号和下划线,还能够用数字0-9。
笼统来说,v8也是经过这个规则来处理标识符,下面就来看看详细的解析过程。
老规矩,代码我丢github上面,接着前面一篇的内容,进行了一些整理,将文件分类,保证下载便可运行。
var复制代码
首先须要完善Token映射表,添加关于标识符的内容,以下。
const TokenToAsciiMapping = (c) => {
return c === '(' ? 'Token::LPAREN' :
c == ')' ? 'Token::RPAREN' :
c == '"' ? 'Token::STRING' :
c == '\'' ? 'Token::STRING' :
IsAsciiIdentifier(c) ? 'Token::IDENTIFIER' :
'Token::ILLEGAL'
};复制代码
在那个超长的三元表达式中添加一个标识符的判断,因为标识符的合法字符较多,因此单独抽离一个方法作判断。
/** * 判断给定字符(数字)是否在两个字符的范围内 * C++经过static_cast同时处理了char和int类型 JS就比较坑了 * 这个方法其实在C++超简单的 然而用JS直接炸裂 * @param {char} c 目标字符 * @param {char} lower_limit 低位字符 * @param {chat} higher_limit 高位字符 */export const IsInRange = (c, lower_limit, higher_limit) => { if(typeof lower_limit === 'string' && typeof higher_limit === 'string') { lower_limit = lower_limit.charCodeAt(); higher_limit = higher_limit.charCodeAt(); } if(typeof c === 'string') c = c.charCodeAt(); return (c >= lower_limit) && (c <= higher_limit);}
/**
* 将大写字母转换为小写字母 JS没有char、int这种严格类型 须要手动搞一下
*/
const AsciiAlphaToLower = (c) => { return String.fromCharCode(c.charCodeAt() | 0x20); }
/**
* 数字字符判断
*/
const IsDecimalDigit = (c) => {
return IsInRange(c, '0', '9');
}
/**
* 大小写字母、数字
*/
const IsAlphaNumeric = (c) => {
return IsInRange(AsciiAlphaToLower(c), 'a', 'z') || IsDecimalDigit(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 'Token::IDENTIFIER':
return ScanIdentifierOrKeyword();
}
}
} 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;复制代码
-
标识符的结束标记,好比')'、'}'等符号都表明这个标识符没了
-
非关键词标记,好比一个标识符包含'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)
}
const character_scan_flags = UnicodeToAsciiMapping.map(c => GetScanFlags(c));复制代码
对照定义,上面的方法基本上不用解释了,用到了我前面讲过的一个技巧bitmap(文盲不懂专业术语,难怪阿里一面就挂了)。因为是按照C++源码写的,上述部分工具方法仍是须要挨个实现。源码用的宏,写起来一把梭,用JS仍是挺繁琐的,具体代码我放github了。
有了这个映射表,后面不少地方就很方便了,如今来实现标识符的解析方法。
实现以前,来列举一下可能出现的标识符:var、vars、avr、1ab、{ '\a': 1 }、吉米(\u5409\u7c73),这些标识符有些合法有些不合法,可是都会进入解析阶段。因此总的来讲,方法首先保证能够处理上述全部状况。
对于数字开头的标识符,其实在case阶段就被拦截了,虽说数字1也会出如今一个IDENTIFIER中,可是1会首先被优先解析成'Token::Number',有对应的方法处理这个类型,以下。
case 'Token::STRING':
return this.ScanString();
case 'Token::NUMBER':
return ScanNumber(false);
case 'Token::IDENTIFIER':
return ScanIdentifierOrKeyword();复制代码
Scanner::ScanIdentifierOrKeyword() {
this.next().literal_chars.Start();
return this.ScanIdentifierOrKeywordInner();
}
Scanner::ScanIdentifierOrKeywordInner() {
let escaped = false;
let can_be_keyword = true;
if(this.c0_ < kMaxAscii) {
if(this.c0_ !== '\\') {
let scan_flags = character_scan_flags[this.c0_];
scan_flags >>= 1;
this.AddLiteralChar(this.c0_);
this.AdvanceUntil((c0) => {
if(c0 > kMaxAscii) {
scan_flags |= kIdentifierNeedsSlowPath;
return true;
}
let char_flags = character_scan_flags[c0];
scan_flags |= char_flags;
if(TerminatesLiteral(char_flags)) {
return true;
} else {
this.AddLiteralChar(c0);
return false;
}
});
if(!IdentifierNeedsSlowPath(scan_flags)) {
if(!CanBeKeyword(scan_flags)) return 'Token::IDENTIFIER';
let chars = this.next().literal_chars.one_byte_literal();
return this.KeywordOrIdentifierToken(chars, chars.length);
}
can_be_keyword = CanBeKeyword(scan_flags);
} else {
escaped = true;
let c = this.ScanIdentifierUnicodeEscape();
if(c === '\\' || !IsIdentifierStart(c)) return 'Token::ILLEGAL';
this.AddLiteralChar(c);
can_be_keyword = CharCanBeKeyword(c);
}
}
return ScanIdentifierOrKeywordInnerSlow(escaped, can_be_keyword);
}复制代码
感受C++的类方法实现的写法看起来很舒服,博客里也这么写了,但愿JavaScript何时也借鉴一下,貌似::在JS里目前还不是一个运算符,总之真香。
首先能够发现,标识符的解析也用到了Literal类,以前说这是用了字符串解析并不许确,所以我修改了AdvanceUntil方法,将callback做为参数传入。启动类后,扫描逻辑以下。
-
一旦字符出现Ascii编码大于127或者转义符号,仍到慢解析方法中
-
对全部字符进行逐个遍历,方式相似于上篇的字符串解析,结束标记略有不一样
-
通常状况下不用慢解析,根据bitmap中的kCannotBeKeyword快速判断返回变量仍是进入关键词解析分支
v8中字符相关的工具方法就单独搞了一个cpp文件,里面方法很是多,后续若是是把v8所有翻译过来估计也要分好多文件了,先这样吧。
先无论慢解析了,大部分状况下也不会用中文作变量,相似于zzz、jjj的变量会快速跳出,标记为"Token::IDENTIFIER"。而多是关键词的标识符,好比上面列举的var、vars、avr,因为或多或少的具备一些关键词特征,会深刻再次解析。
须要说的是,从一个JavaScript使用者的角度看,关键词的识别只须要对字符串作严格对等比较就好了,好比长度3,字符顺序依次是v、a、r,那么一定是关键词var。
可是v8的实现比较迷,用上了Hash,既然是v8体验文章,那么就按照源码的逻辑实现上面的KeywordOrIdentifierToken方法。
Scanner::KeywordOrIdentifierToken(str, len) {
return PerfectKeywordHash.GetToken(str, len);
}
const MIN_WORD_LENGTH = 2;
const MAX_WORD_LENGTH = 10;
class PerfectKeywordHash {
static GetToken(str, len) {
if(IsInRange(len, MIN_WORD_LENGTH, MAX_WORD_LENGTH)) {
let key = PerfectKeywordHash.Hash(str, len) & 0x3f;
if(len === kPerfectKeywordLengthTable[key]) {
const s = kPerfectKeywordHashTable[key].name;
let l = s.length;
let i = -1;
while(i++ !== l) {
if(s[i] !== str[i]) return 'Token::IDENTIFIER';
}
return kPerfectKeywordHashTable[key].value;
}
}
return 'Token::IDENTIFIER';
}
}复制代码
整体逻辑如上所示,关键词的长度目前是2-10,因此根据长度先筛一波,再v8根据传入的字符串算出了一个hash值,而后根据这个值从映射表找出对应的特征,对二者进行严格对对比,来断定这个标识符是否是一个关键词。
涉及1个hash算法和2个映射表,这里把hash算法给出来,映射表实在是繁琐,有兴趣去github看吧。
static Hash(str, len) {
const asso_values = [
56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56,
56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56,
56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56,
56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56,
56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56,
56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56,
56, 8, 0, 6, 0, 0, 9, 9, 9, 0, 56, 56, 34, 41, 0, 3,
6, 56, 19, 10, 13, 16, 39, 26, 37, 36, 56, 56, 56, 56, 56, 56,
];
return len + asso_values[str[1].charCodeAt()] + asso_values[str[0].charCodeAt()];
}复制代码
能够看到,hash方法的内部也有一个映射表,每个关键字符都有对应的hash值,经过前两个字符进行运算(最短的关键词就是2个字符,而且),获得一个hash值,将这个值套到另外的table获得其理论上的长度,长度一致再进行严格比对。
这个方法我的感受有一些微妙,len主要是作一个修正,由于前两个字符同样的关键词仍是蛮多的,好比说case、catch,delete、default等等,可是长度不同,加上len能够区分。若是有一天出现前两个字符同样,且长度也同样的关键词,这个hash算法确定要修改了,反正也不关我事咯。
通过这一系列的处理,标识符的解析算是完成了,代码能够github上面下载,而后修改test文件里面传入的参数就能看到输出。