原文:http://ariya.ofilabs.com/2012/02/from-double-quotes-to-single-quotes.html
程式碼的不一致性總是讓人發狂,如果每位開發者都能遵守約定好的編碼規範(coding conventions),那麼生活將變的更加美好.比如在JavaScript中,一個字串字面量可以用單引號引起,也可以用雙引號來引起(ECMAScript 5規範7.8.4小節).很多人習慣於使用某種特定的引號,比如jQuery編碼風格推薦人們使用雙引號.
但我個人更喜歡使用單引號,這僅僅是我的偏好.自從有了Esprima,我意識到,我可以利用Esprima能夠以非破壞式(non-destructive)的方式對輸入的JavaScript原始碼進行區域性修改(partial modification)的能力,來強制讓輸入JavaScript原始碼中的每個字串字面量都使用單引號.於是我就寫出了下面的singlequote.js指令碼:
var fs = require('fs'), esprima = require('esprima'), input = process.argv[2], output = process.argv[3], offset = 0, content = fs.readFileSync(input, 'utf-8'), tokens = esprima.parse(content, { tokens: true, range: true }).tokens; function convert(literal) { var result = literal.substring(1, literal.length - 1); result = result.replace(/'/g, '\''); return ''' + result + '''; } tokens.forEach(function (token) { var str; if (token.type === 'String' && token.value[0] !== '\'') { str = convert(token.value); content = content.substring(0, offset + token.range[0]) + str + content.substring(offset + token.range[1] + 1, content.length); offset += (str.length - token.value.length); } }); fs.writeFileSync(output, content);
這個指令碼需要用Node.js來執行,像這樣:
node singlequote.js inputfile outputfile
該指令碼具體是如何工作的?讓我們假設輸入原始碼的內容是這樣的:
console.log("Hello")
在我們把這句程式碼傳入Esprima解析器的時候,要把其中一個解析選項tokens設為true,這樣解析器才會在解析的過程中把遇到的所有token(詞法單元)收集到一個陣列中,並返回它.對於我們上面的這句程式碼,返回的token陣列看起來是這樣的:
[ { type: "Identifier", value: "console", range: [0, 6] }, { type: "Punctuator", value: ".", range: [7, 7] }, { type: "Identifier", value: "log", range: [8, 10] }, { type: "Punctuator", value: "(", range: [11, 11] }, { type: "String", value: ""Hello"", range: [12, 18] }, { type: "Punctuator", value: ")", range: [19, 19] } ]
譯者注:在編譯原理領域中,token這個詞可以被翻譯成詞法單元或者詞法記號,它表示一個在原始碼中擁有獨立意義的最小單位,是不可再分的詞法單元(lexical unit).一個token是由若干個字元組成的,就像英文中的單詞一樣,所以也有人直接把它翻譯成單詞.在ES標準中,token具體包含有保留字,識別符號,字面量,標點符號這幾種輸入元素(input element).
一旦我們拿到了所有的token物件,剩下的事情就簡單多了.我們只需要遍歷這個token物件陣列,找到那些與某個字串字面量關聯的token(type屬性為String的token物件).每個token物件都會在自己的range屬性中存放有所關聯字串字面量的位置資訊,這是一個表示了所關聯字串字面量在輸入原始碼中的開始位置和結束位置的索引區間陣列(閉區間).
{ type: "String", value: ""Hello"", range: [12, 18] }
有了這個位置資訊,我們就能使用一些基本的字串操作來替換輸入原始碼中的某段內容.對於上面這個例子的話,修改目標就是索引位置在[12, 18]之間的原始碼內容.值得注意的是,如果原始字串字面量的值(兩個引號中間夾著的內容)中包含有一個或多個的單引號,則我們需要做一些額外的工作,就是要把這些單引號進行轉義(檢視第7.8.4小節的SingleEscapeCharacters).如果真的需要進行這樣的轉義,則轉義後的字串字面量的長度會大於原始字串字面量的長度(多了反斜槓字元),從而改變了整個原始碼的長度,再從而讓token陣列中其它還未處理的token物件中包含的位置資訊產生錯位,因此我們還需要進行偏移量的調整.下面是個需要進行轉義的字串字面量的例子:
// 輸入原始碼 "color = 'blue'"; // 不進行轉義操作的話會輸出非法的字串字面量 'color = 'blue'';
另外,我寫的轉換程式碼還差一件事情沒做,就是要把那些不再需要的轉義字元也刪除掉.也就是原來字串字面量中包含的雙引號前面的那個反斜槓,它已經不再需要了.這項工作就留給讀者們實現吧!
譯者注:作者說的是這種情況,原字串字面量為"\"",這時用反斜槓轉義裡面的雙引號是必須的,但按照上面的規則轉換之後就成了'\"',雖然這也是一個合法的字串字面量,且求值結果不變.不過作者的意思是這個反斜槓是多餘的,是應該刪除掉的.
很顯然,我寫的這個工具只是為教學演示而用.另外,雖然現在大部分編輯器都支援搜尋替換的功能,如果你需要使用編輯器來完成這項任務,也有一定的難度,注意不要替換掉那些不在字串字面量兩邊的引號.
譯者注:你覺的能用正規表示式來完成這項任務嗎?註釋和正則字面量中的引號字元會讓你束手無策.
你還可以想想看,利用token列表和原始碼區域性修改的技術,還能幹哪些事情?
譯者注:你有沒有發現作者忽略了一個比較極端的情況,就是假如原字串字面量為"\'",雖然這個反斜槓是多餘的,但這的確是一個合法的字串寫法,求值後字串的值為一個單引號.按照上面的演算法轉換之後會變成'\\''.顯然,這會導致一個語法錯誤,因為少了一個反斜槓.是不是呢?
有了Esprima,實現這樣一個轉換器真的是很簡單,如下