從 ES1 到 ES5 的這 14 年時間裏,Function.prototype.toString 的規範一字未變:javascript
An implementation-dependent representation of the function is returned. This representation has the syntax of a FunctionDeclaration. Note in particular that the use and placement of white space, line terminators, and semicolons within the representation String is implementation-dependent.
這段話說了兩點內容:java
1. toString() 返回的字符串應該符合 FunctionDeclaration 的語法git
2. 要不要保留原始的空白符和分號,規範無論github
先說第一點,規範管的。FunctionDeclaration 就是咱們一般說的函數聲明,語法是這樣的:express
function BindingIdentifier (FormalParameters) { FunctionBody }
規範要求全部函數 toString() 時返回的字符串都得符合函數聲明的語法,但其實從 1995 年到今天沒有一個 JS 引擎作到過,違背這個約束的主要有下面兩種狀況:瀏覽器
1. 匿名函數表達式 toString() 時返回的是 FunctionExpression 而不是 FunctionDeclarationasync
var f = function (){} f.toString() // "function (){}"
"function (){}" 不符合函數聲明的語法,由於缺乏函數名,返回的其實是個函數表達式,直到如今全部引擎也都這樣。函數
額外小知識:V8 去年實現過將推斷出的函數名放到 function 和 參數列表之間,後來又刪了 優化
2. 內置函數、宿主函數、綁定函數 toString() 時返回的 FunctionDeclaration 不合法ui
Object.toString() // "function Object() { [native code] }" alert.toString() // "function alert() { [native code] }" (function (){}).bind().toString() // "function () { [native code] }"
包含 [native code] 字樣的函數體顯然不是合法的 JS 語法,更不可能符合 FunctionDeclaration,實際上內置函數和宿主函數根本不是用 JS 寫的,他們不可能有真正的函數體。
這兩點都是須要規範來澄清的,esdiscuss 上也有過屢次討論,ES4 的規範草案曾經專門澄清過第一點:
Description
The intrinsic
toString
method converts the executable code of the function to a string representation. This representation has the syntax of a FunctionDeclaration or FunctionExpression. Note in particular that the use and placement of white space, line terminators, and semicolons within the representation string is implementation-dependent.COMPATIBILITY NOTE ES3 required the syntax to be that of a FunctionDeclaration only, but that made it impossible to produce a string representation for functions created from unnamed function expressions.
也就是說 ES4 想把曾經限制的 FunctionDeclaration 擴展成 「FunctionDeclaration 或 FunctionExpression」,但後來的事你就知道了,ES4 流產了,ES5 並無改 ES3 裏的這一段話。
關於空白符和分號的處理,引擎愛怎麼實現就怎麼實現,好比下面這個簡單的函數:
function f (){return 1} f.toString() // Chrome 下是 "function f(){return 1}",函數名右邊的空白符沒了,左邊也只剩下一個空格 // Firefox 17 前曾是 "function f() {\n return 1;\n}",除了同上面 Chrome 相同的一點外,函數體內多了一些空白符,還多了個分號 // Firefox 17 以後是 "function f(){return 1}",和 Chrome 同樣了 // IE 全部版本都是 "function f (){return 1}",源代碼原封不動返回
實際上各引擎實現有差別的不止空白符、分號這兩個語法元素,還有註釋,甚至還有常規的語句,好比:
function f() { // foo /* bar */ 1+2 return 2 + 2 } console.log(f.toString())
上面的代碼在 Firefox 17 以前輸出會是:
function f() { return 4; }
函數體內部只剩下了一行,註釋都丟了,一些代碼也被優化了。
還有下面的代碼:
(function() { "use strict" function f() {1+1} console.log(f.toString()) })()
在 Firefox 48 以前會輸出:
function f() { "use strict";
1+1}
就是說它會把繼承自上層做用域的嚴格模式在本身的源碼中體現出來。
還有各類曾經的瀏覽器有着各類各樣的奇怪表現,kangax 在 09 年和 14 年分別寫文章講過 http://perfectionkills.com/those-tricky-functions/ http://perfectionkills.com/state-of-function-decompilation-in-javascript/ 時到現在,研究這些歷史表現已經意義不大了,咱們通通跳過。
能夠這麼說,函數的 toString() 方法在 ES6 以前就沒有規範。ES6 中引入了箭頭函數、生成器函數、類等 7 種新的函數語法,同時對函數的 toString() 方法作了更詳細的規定:
The string representation must have the syntax of a FunctionDeclaration, FunctionExpression,GeneratorDeclaration, GeneratorExpression, ClassDeclaration, ClassExpression, ArrowFunction,MethodDefinition, or GeneratorMethod depending upon the actual characteristics of the object.
The use and placement of white space, line terminators, and semicolons within the representation String is implementation-dependent.
If the object was defined using ECMAScript code and the returned string representation is not in the form of a MethodDefinition or GeneratorMethod then the representation must be such that if the string is evaluated, using
eval
in a lexical context that is equivalent to the lexical context used to create the original object, it will result in a new functionally equivalent object. In that case the returned source code must not mention freely any variables that were not mentioned freely by the original function’s source code, even if these 「extra」 names were originally in scope.If the implementation cannot produce a source code string that meets these criteria then it must return a string for which
eval
will throw a SyntaxError exception.
第一點是對舊規範的澄清,說返回的字符串沒必要須是函數聲明瞭;第二點沒變化;第三四點是新加的,三是說一個函數 fn 和經過 eval(fn.toString()) 生成的新函數功能要等效;四是說假如引擎作不到前面規定的這些,那就必須讓 toString() 返回一個包含非法語法的字符串,即向前不兼容。
但其實 ES6 裏的新規定仍然很模糊,好比說兩個函數功能等效,那究竟啥是功能等效,還有仍然無論空白符和分號,這些致使各瀏覽器中 toString() 的返回結果仍然能夠是五花八門。
ES6 以後,一個新的提案嘗試對 Function.prototype.toString 進行真正的規定,目前在 Stage 3 階段,Chrome 和 Firefox 已經基本實現了這一提案,其實這個新的規範很好記憶:
1. 凡有完整源碼的,一字不落把源碼返回,好比:
function f (){return 1} "function f (){return 1}" === f.toString() // true
Chrome 和 Firefox 之前都是把從參數列表左側的那個小括號開始到函數體右側那個大括號結束的源碼保存下來,用的時候前面補上了「function 函數名」,如今是從 「function」 關鍵字就開始保存源碼,若是是 async function,會從 「async」 關鍵字開始保存。
若是是方法,會從方法名開始保存;若是是生成器方法,會從 * 號開始保存;若是是 getter/setter,會從 「get」 或 「set」 開始保存:
({m/*註釋*/(){}}).m.toString() // "m/*註釋*/(){}" ({* g/*註釋*/(){}}).g.toString() // "* g/*註釋*/(){}" Object.getOwnPropertyDescriptor({get/*A*/f/*B*/(/*C*/ /*D*/)/*E*/{/*F*/}}, "f").get.toString() // "get/*A*/f/*B*/(/*C*/ /*D*/)/*E*/{/*F*/}"
總之最核心的理念就是,源碼是什麼,toString() 就返回什麼,ES6 裏曾經要求的什麼「功能等效」和「向前不兼容」,所有做廢。
2. 經過 Function()/GeneratorFunction()/AsyncFunction() 這些「函數的構造函數」動態生成的函數(沒有真實的源碼)在 toString() 時返回什麼,這個提案也作了詳細的規定,沒有模棱兩可的地方。
Function("a","b","a+b").toString() /* function anonymous(a,b ) { a+b } */
基本上就是 "function anonymous(" + 參數名列表.join(",") + ""\n) {\n" + 函數體 + "\n}"
3. 內置函數、宿主函數、綁定函數返回的函數體得是 { [native code] },不過這其中的空白符能夠任意放置,用代碼來講話的話,這些函數 toString() 的返回結果要能匹配下面這個正則:
/\bfunction\b[\s\S]*\([\s\S]*\)[\s\S]*\{[\s\S]*\[[\s\S]*\bnative\b[\s\S]+\bcode\b[\s\S]*\][\s\S]*\}/
本文故意省略不少細枝末節,讀完以後你只要記的一句就夠了:「Function.prototype.toString 已經有了嚴格的規範,規範的核心就是函數的源碼是什麼就返回什麼」。