从 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. Java的动态代理

    什么是动态代理(dynamic proxy) 动态代理(以下称代理),利用Java的反射技术(Java Reflection),在运行时创建一个实现某些给定接口的新类(也称“动态代理类”)及其实例(对 ...

  2. web框架开发-分页器(Paginator)

    Django有自带的分页器,可以将数据分在不同的页面中,并提供一些属性和方法实现对分页数据的操作.分页功能的类位于django/core/paginator.py中. 常用方法 # 分页器 # pag ...

  3. Django-CRM项目学习(四)-stark的分页器与搜索框

    1.分页器 分页器相关知识点,请查看以下链接 https://www.cnblogs.com/gbq-dog/p/10724859.html 2.代码归类 归类前代码 header_list = [] ...

  4. SQL瓶颈分析,以及适应最佳执行计划的探讨

    原文地址:   https://blog.csdn.net/daiqiulong2/article/details/86546446?tdsourcetag=s_pcqq_aiomsg 年纪大了,慢慢 ...

  5. bsxfun

    By HYB bsxfun(fun,A,B)偶然间发现了这个函数,强大得不得了呀,它的作用是:对两个矩阵A和B之间的每一个元素进行指定的计算(函数fun指定):并且具有自动扩维的作用 例如,A是一个4 ...

  6. Strem_01

    import 'package:flutter/material.dart';import 'dart:async';import 'dart:ui'; void main()=>runApp( ...

  7. Docker启动Get Permission Denied

    https://www.cnblogs.com/informatics/p/8276172.html 以下问题及解决方法都在Ubuntu16.04下,其他环境类似 问题描述 安装完docker后,执行 ...

  8. 用bisect维护一个排序的序列

    import bisect list1 = [] bisect.insort(list1, 5) bisect.insort(list1, 1) bisect.insort(list1, 3) bis ...

  9. eclipse(STS)安装jd-eclipse插件实现查看API源代码功能

    emmm,IDEA确实是比STS智能很多,不过适当的转化也是需要的,这里介绍一下eclipse(STS)实现查看class反编译的源文件的功能 去Java Decompiler官网下一下eclipse ...

  10. spring中设计模式

    MVC模式 Model:pojo.数据库交互(业务数据和业务逻辑) View:Jsp(与用户交互页面) Controller:控制器(接收请求并决定调用哪个模块组件去处理请求,然后决定调用哪个视图(通 ...