每次為 JSHint 提交程式碼我都會學到一些有關 JavaScript 的新知識。最近的一次知識之旅中我接觸到了函式物件的 name
屬性。
JSHint 有一個很有意思但鮮為人知的功能:程式碼分析報告。當以程式設計方式使用這項功能時,JSHint 會返回一個物件,描述已分析程式碼的資料。它包括(但不限於)程式碼中函式物件的資訊:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
jshint('function myFn() {}'); console.log(jshint.data().functions); /* [{ name: 'myFn', param: undefined, line: 1, character: 15, last: 1, lastcharacter: 19, metrics: { complexity: 1, parameters: 0, statements: 0 } }] */ |
JSHint 網站本身實時生成的“Metrics”報告是這個功能最突出的應用。比如:
Metrics
- There is only one function in this file.
- It takes no arguments.
- This function is empty.
- Cyclomatic complexity number for this function is 1.
我得知這個功能在與一個不相關的 bug 一起工作時會出錯。更困擾我的是,我發現自己以前對 JavaScript 函式名的理解完全是錯誤的。對我的三觀質疑了幾個小時(“名字意味著什麼?”、“我是真實存在的麼?”….)之後,我決定開始研究這個問題,徹底瞭解其正確的行為方式。下面是我所學到的東西。
你以為自己知道…
首先我該解釋一下我一開始對 name 如何在 JavaScript 中分配的誤解。
我以前只知道函式物件間的一個區別 —— 函式宣告與函式表示式。前者需要一個識別符號,所以我通常認為這是一個“命名函式”:
1 2 3 |
function myFunction() { } |
而後者不需要識別符號,我就把它叫作“匿名函式”:
1 2 3 |
(function() { }()); |
這種推斷在直覺上是合理的,因為它用了直白的詞語定義如“命名的”和“匿名的”。這也許可以解釋為何不止我一個人存在這個誤解。事實上:現在的 JavaScript (ECMAScript 5.1 或縮寫 ES5)對於函式的 name
屬性並沒有明確的說明。稍微看看這個相關的說明就可以支援我的觀點。我們一般認為命名函式表示式中的識別符號會指向“name”,實際上它只會被用在環境記錄(environment record)中建立一個入口(entry),(跟 var
宣告一樣)。除此以外的說明都會存在與特定平臺相關的細微差異。
(三觀已崩潰)
…你根本不知道
碰巧下一代 JavaScript (即 ES6 ,工作草案在這裡)的說明中明確了函式 name
屬性的初始化。很方便的是,它完全依賴於一個叫 SetFunctionName 的抽象操作。學習函式名賦值的來龍去脈其實就是簡單(雖然也比較無聊)地去研究草案中關於這個操作的所有文獻。對於平臺實現者來說這必不可少,但對於我們來說,稍微學習幾個例子就足夠了。
首先,這個規範對一些我們可以預料到的行為作了規定:
1 2 3 |
// 函式形式 ......................... name 屬性的值 function myFunc() {} // 'myFunc; (function() {}()); // '' |
但遠不止如此!這份規範還列出了在一系列情況下,函式表示式(我前面所以為的“匿名函式”)也應該被賦予 name
值:
1 2 3 4 5 6 7 8 9 10 |
// 函式形式 ......................... name 屬性的值 new Function(); // 'anonymous' var toVar = function() {}; // 'toVar' (function() {}).bind(); // 'bound' var obj = { myMethod: function() {}, // 'myMethod' get myGetter() {}, // 'get myGetter' set mySetter(value) {} // 'set mySetter' }; |
但這裡要清楚,新規範只會在以上這些情況下才改變函式物件的 name
屬性。至於現在的 ES5 語法,環境記錄依然會保持不變,只有函式宣告才會產生新入口。
這讓我很驚訝,因為不像函式宣告,我從來沒有想到對變數或屬性的賦值會與函式物件的建立有聯絡。但 ES6 就是這麼任性!JSHint 團隊將這個行為稱作“名推斷”(name inference)。函式物件本身沒有讓識別符號定義,而是由執行時通過其初始賦值去對函式的名字做“最佳猜測”。
最後,ES6 定義了一大堆不相容 ES5 的新程式碼格式。當中一部分進一步擴充套件了函式名推斷的語義:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// 函式形式 ......................... name 屬性的值 let toLet = function() {}; // 'toLet' const toConst = function() {}; // 'toConst' export default function() {} // 'default' function* myGenerator() {} // 'myGenerator' new GeneratorFunction() {} // 'anonymous' var obj = { ['exp' + 'ression']: function() {}, // 'expression' myConciseMethod() {}, // 'myConciseMethod' *myGeneratorMethod() {} // 'myGeneratorMethod' }; class MyClass { constructor() {} // 'MyClass' myClassMethod() {} // 'myClassMethod' } |
最後一個例子讓我很吃驚,建構函式居然被賦予類的名字而不是“constructor”?對於其它大多數的類方法,其 name
值跟你想的基本一樣。但構造方法很特殊,因為他們本質上都是引用其歸屬的類。這在 ES5 中也有相應的例子:
1 2 |
function MyClass() {} MyClass.prototype.constructor === MyClass; |
這個原則同樣適用於 ES6 ,即便建構函式體與 class
關鍵字出現在不同的表示式中。
標準偏差
手中有了完整的規格書,我們就可以重新看看 JSHint 中函式名推斷的過程。注意它也不是一模一樣照搬實現的,有些地方我們是故意做得跟規範不同。
「表示式」:很多情況下,實現者會直接以表示式的結果去呼叫 SetFunctionName
。(如:“設 propKey
為對 PropertyName
求值的結果。[…] SetFunctionName(propValue, propKey)
。”)且因為 JSHint 是一個靜態分析工具,它不會對檢測的程式碼做任何計算(見文末)。所以這種情況下,我們會報告此函式的 name
為“(expression)”。
「未命名」:規範要求“若 description
值為 undefined ,則 name
值取空字串。”這裡意思是類似下面的函式宣告:
1 2 3 |
(function() { })(); |
其 name
值應該為 “” 。但我們決定對這種函式的 name
報告為“(empty)”。因為 JSHint 這個工具的目的是協助開發者而非JavaScript執行環境,我們覺得在這種情況下將規範作重新解釋是可以接受的。具體來說:JSHint 在其報告中賦予函式的名字不會引起相容性問題,所以我們實現了不同的行為,因為這樣做更有幫助。
改進的函式名推斷已經在 JSHint 的 master 分支中 landed 了,可以展望它會出現在下個釋出中。
其它名字的函式
我從不厭倦去閱讀下一代 JavaScript 的各種炫酷新特性。即便如此,對比起 generator、class、module 和 promise ,函式名的確顯得有些過時。悲觀者甚至會認為這是語言中的一個沒有必要的累贅。但正如任何優秀的標準,這個新特性實際上也是一種現實需求的體現。
報錯的棧跟蹤裡需要函式名。缺少函式名推斷的情況下,平臺一般會用一些通用的替換值如“(anonymous function)”去報告沒有名字的函式。這往往會從整體上削弱了棧跟蹤的實用性。現在的一些效能檢測工具和控制檯會識別一個叫 displayName
的非標準值,並在棧跟蹤時回退到該值。Malte Ubl 最近也贊成將此納入 JavaScript 庫程式碼,Ember.js 也對此稍作嘗試。
但隨著執行環境實現了新功能,諸如此類的非標準方法就變得沒什麼必要了。這個小小的改變可以幫助開發者專注於著手解決問題而無需擔心怎麼減少除錯陷阱。所以即使在即將到來的各種 JavaScript 會議中你不太可能會見到標題為“ES6 中的函式名推斷”之類的演講,這個小小的特性依然值得慶祝。
- JSHint 的確會對封閉字串做連線操作,但這基本算不上是程式碼執行。