从 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 已经有了严格的规范,规范的核心就是函数的源码是什么就返回什么”。

esnext:Function.prototype.toString 终于有规范了的更多相关文章

  1. Function.prototype.toString 的使用技巧

    Function.prototype.toString这个原型方法可以帮助你获得函数的源代码, 比如: function hello ( msg ){ console.log("hello& ...

  2. Function.prototype.toString

    语法:fn.toString(indentation) 改方法返回当前函数源代码的字符串,而且还可对此字符串进行操作,比如: function num(){ }; var str = num.toSt ...

  3. 判断js中各种数据的类型方法之typeof与0bject.prototype.toString讲解

    提醒大家,Object.prototype.toString().call(param)返回的[object class]中class首字母是大写,像JSON这种甚至都是大写,所以,大家判断的时候可以 ...

  4. JavaScript:Object.prototype.toString方法的原理

    在JavaScript中,想要判断某个对象值属于哪种内置类型,最靠谱的做法就是通过Object.prototype.toString方法. var arr = []; console.log(Obje ...

  5. JavaScript:Object.prototype.toString进行数据类型判定

    在JavaScript中,想要判断某个对象值属于哪种内置类型,最靠谱的做法就是通过Object.prototype.toString方法. var arr = []; console.log(Obje ...

  6. JavaScript中Object.prototype.toString方法的原理

    在JavaScript中,想要判断某个对象值属于哪种内置类型,最靠谱的做法就是通过Object.prototype.toString方法. ? 1 2 var arr = []; console.lo ...

  7. Object.prototype.toString.call(obj)使用方法以及原理

    这几天看vue-router的源码 发现了Object.prototype.toString.call()这样的用法,当时以为这就是转成字符串的用的,但是越看越觉得不太对劲,赶紧查查资料,一查才知道没 ...

  8. js Object.prototype.toString.call()

    Object.prototype.toString.call(obj)使用方法以及原理   这几天看vue-router的源码 发现了Object.prototype.toString.call()这 ...

  9. Object.prototype.toString.call(arg)详解

    经常能碰到Object.prototype.toString.call对参数类型进行判断,一开始只知道怎么使用,却不了解具体实现的原理,最近恶补了一下相关知识,写个笔记加强理解,有什么不对的请指教. ...

随机推荐

  1. Netty(RPC高性能之道)原理剖析

    转载:http://blog.csdn.net/zhiguozhu/article/details/50517551 1,Netty简述 Netty 是一个基于 JAVA NIO 类库的异步通信框架, ...

  2. web框架开发-Django的Forms组件

    校验字段功能 针对一个实例:用户注册. 模型:models.py class UserInfo(models.Model): name=models.CharField(max_length=32) ...

  3. 部署Java和Tomcat

    Tomcat介绍 Tomcat服务器是一个免费的开放源代码的Web应用服务器,在中小型系统和并发访问用户不是很多的场合下被普遍使用,是开发和调试JSP网页的首选. Tomcat和Nginx.Apach ...

  4. Topshelf:一款非常好用的 Windows 服务开发框架

    背景 多数系统都会涉及到“后台服务”的开发,一般是为了调度一些自动执行的任务或从队列中消费一些消息,开发 windows service 有一点不爽的是:调试麻烦,当然你还需要知道 windows s ...

  5. Python调用接口的几种方式

    1. requests import requests, jsongithub_url = 'https://api.github.com/user/repos'data = json.dumps({ ...

  6. Struts2的核心——拦截器

    虽然以前已经学了很多的拦截器,但是在这里还是想重头梳理一下所有有关拦截器的知识,尤其是struts2中的拦截器 1:拦截器是什么? java里的拦截器是动态拦截Action调用的对象.它提供了一种机制 ...

  7. visual studio 各版本激活码

    visual studio 各版本 激活码 版本 产品型号 激活码 vs2019 Enterprise(企业版) BF8Y8-GN2QH-T84XB-QVY3B-RC4DF vs2019 Profes ...

  8. windows下安装和使用scrapy

    首先,要确保已经正确安装了python环境,并安装了pip包 接着,打开cmd或者powershell ,输入命令 pip install scrapy .安装完之后 运行scrapy性能测试命令: ...

  9. mybatis mapper映射文件全解

    目录 select.update.delete.insert 设置参数类型以及取值 基本数据类型 对象数据类型 map数据类型 #{  } 和 ${  } 的区别 ResultMap Auto-map ...

  10. 解决虚拟机下安装CentOS无法上网

    Centos7默认是不启用有线网卡的,需要手动开启. 操作步骤如下: 首先,打开终端.cd /etc/sysconfig/network-scripts/ls 查看一下ifcfg-eno后面对应的数字 ...