esnext:Function.prototype.toString 终于有规范了

从 ES1 到 ES5 的这 14 年时间里,Function.prototype.toString 的规范一字未变:

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.

这段话说了两点内容:

1. toString() 返回的字符串应该符合 FunctionDeclaration 的语法

2. 要不要保留原始的空白符和分号,规范不管

规范管的一点引擎们从来没遵守

先说第一点,规范管的。FunctionDeclaration 就是我们通常说的函数声明,语法是这样的:

function BindingIdentifier (FormalParameters) { FunctionBody }

规范要求所有函数 toString() 时返回的字符串都得符合函数声明的语法,但其实从 1995 年到今天没有一个 JS 引擎做到过,违背这个约束的主要有下面两种情况:

1. 匿名函数表达式 toString() 时返回的是 FunctionExpression 而不是 FunctionDeclaration

var f = function (){}
f.toString() // "function (){}"

"function (){}" 不符合函数声明的语法,因为缺少函数名,返回的实际上是个函数表达式,直到现在所有引擎也都这样。

额外小知识:V8 去年实现过将推断出的函数名放到 function 和 参数列表之间,后来又删了 

2. 内置函数、宿主函数、绑定函数 toString() 时返回的 FunctionDeclaration 不合法

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/ 时到如今,研究这些历史表现已经意义不大了,我们统统跳过。

ES6 的澄清

可以这么说,函数的 toString() 方法在 ES6 之前就没有规范。ES6 中引入了箭头函数、生成器函数、类等 7 种新的函数语法,同时对函数的 toString() 方法做了更详细的规定:

  • The string representation must have the syntax of a FunctionDeclarationFunctionExpression,GeneratorDeclaration, GeneratorExpression, ClassDeclarationClassExpressionArrowFunction,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 已经有了严格的规范,规范的核心就是函数的源码是什么就返回什么”。

posted @ 2017-03-29 16:36  紫云飞  阅读(871)  评论(0编辑  收藏  举报