上一篇講了字符串的解析過程,這一篇來說講標識符(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文件裏面傳入的參數就能看到輸出。