这篇文章是对function-implementation-hiding
提案现状的归纳,在12月TC39会议上,它可能成为stage-3的提案,成为stage-3的提案修改为本会很是高,但愿能引发更多开发者思考,有关心的问题尽早提出来,让JS变得更好。javascript
函数实现隐藏,这个提案在目前已有的指令use strict
基础上,增长两个新指令hide source
和sensitive
(这两个名字是暂时的,在这个issue #3里有讨论,目前你们比较能接受这两个名字),它提供了一种方式,让开发者能够控制某些实现细节不暴露给用户,举个例子:java
function foo() { /* ... */ }
console.assert(foo.toString().contains('...'))
复制代码
若是咱们使用了hide source
指令node
’hide source‘
function foo() { /* ... */ }
console.assert(!foo.toString().contains('...'))
复制代码
添加这个指令有什么好处呢?对于某些开源项目的做者来讲,他们不但愿用户在使用时会依赖具体实现的细节,不然在重构后可能会发生breaking change。还有一些对安全比较敏感的项目,经过隐藏代码实现细节能够规避一些问题,还有polyfill的做者但愿实现“高保真”的polyfill,他们可能出于不一样的目的都想将代码的具体实现细节对使用者隐藏。git
hide source
经过隐藏Function.prototype.toString
的输出,而且隐藏Error.prototype.stack
中的文件属性和位置信息达到隐藏实现细节的目的。sensitive
一样会隐藏Function.prototype.toString
的输出,而且彻底从Error.prototype.stack
中省略了函数。随着新的安全问题出现,sensitive
指令的功能可能会被进一步扩展。es6
正常的Function.prototype.stack
👇github
$ node
Welcome to Node.js v12.13.0.
> console.log((new Error).stack)
Error
at repl:1:14
at Script.runInThisContext (vm.js:116:20)
at REPLServer.defaultEval (repl.js:404:29)
at bound (domain.js:420:14)
at REPLServer.runBound [as eval] (domain.js:433:12)
at REPLServer.onLine (repl.js:715:10)
at REPLServer.emit (events.js:215:7)
at REPLServer.EventEmitter.emit (domain.js:476:20)
at REPLServer.Interface._onLine (readline.js:316:10)
at REPLServer.Interface._line (readline.js:693:8)
复制代码
使用hide source
后Function.prototyoe.stack
👇浏览器
$ node
Welcome to Node.js v12.13.0.
> console.log((new Error).stack)
Error
at repl:1:14
at Script.runInThisContext (vm.js:116:20)
at REPLServer.defaultEval (repl.js:404:29)
at anonymous // 注意这里
at REPLServer.runBound [as eval] (domain.js:433:12)
at REPLServer.onLine (repl.js:715:10)
at REPLServer.emit (events.js:215:7)
at REPLServer.EventEmitter.emit (domain.js:476:20)
at REPLServer.Interface._onLine (readline.js:316:10)
at REPLServer.Interface._line (readline.js:693:8)
复制代码
使用sensitive
后Function.prototype.stack
👇安全
$ node
Welcome to Node.js v12.13.0.
> console.log((new Error).stack)
Error
at repl:1:14
at Script.runInThisContext (vm.js:116:20)
at REPLServer.defaultEval (repl.js:404:29) // 结合上面的代码对比
at REPLServer.runBound [as eval] (domain.js:433:12)
at REPLServer.onLine (repl.js:715:10)
at REPLServer.emit (events.js:215:7)
at REPLServer.EventEmitter.emit (domain.js:476:20)
at REPLServer.Interface._onLine (readline.js:316:10)
at REPLServer.Interface._line (readline.js:693:8)
复制代码
上面的例子是champion在issue中的回复,但因为Error.prototype.stack
不是事实上的标准,各个浏览器实现的都有些差别,因此hide source
和sensitive
最终做用在Error.prototype.stack
的效果可能和上面展现的并不同,关于这方面的讨论请看#33。bash
JavaScript的Function.prototype.toString
保留着实现该函数的源代码,这就让调用者获得没必要要的能力,可以观察到函数的实现细节,他们能够内省函数的实现并对它做出反应,这就会致使项目做者一些无害的重构,却给使用者形成了breaking change的麻烦。使用者甚至能够从源码中提取一些比较隐秘的值,这严重破坏了应用程序的封装。举个例子,Angular依赖f.toString()
输出的源码内省出函数参数名,而后做为框架依赖注入功能的一部分,这就会致使刚才提到的问题。框架
另外一个问题是经过f.toString()
的输出能够判断函数是否为native实现,好比console.log(Math.random.toString())
输出中的字符串中包含[native code]
而不是函数的源码,而自定义的函数就会输出源码,这样就会让”高保真“polyfill变得困难,为此有些polyfill的开发者会替换Function.prototype.toString
或者实现本身的polyfillFn.toString()
。
JavaScript的(事实上是非标准的)Error.prototype.stack
getter 展现了存在或不存在堆栈信息状况下的调用行为。对于递归调用函数,会在堆栈展现递归调用的次数。若是调用行为依赖了某些秘密的值,不管是在源码层面仍是词法层面都会致使这个秘密的值被部分或者所有暴露,另外它还会带出一些文件属性的位置信息,和 toString
以相似的方式影响着重构。
解决方正文开始就谈到了,使用hide source
或者sensitive
指令,改变函数toString()
的输出从而阻止其暴露出具体的实现细节,这两个指令像use strict
同样能够用在整个文件或者某个函数中,一样是向下做用,从而使其做用域内的全部内容以及在函数做用域内的函数自己都被隐藏实现细节,举个例子:
function foo() {
const x = () => {};
const y = () => {
"hide source";
class Z {
m() {}
}
};
}
复制代码
在这个例子中foo
和x
都不会被隐藏实现细节,而y
和Z
以及Z.prototype.m
都会被隐藏实现细节。
该提议很大程度上借鉴了JavaScript现有的指令支持的优点:
hide source
或者sensitive
。这个方案引入了Error.hideFromStackTraces
和 Function.prototype.hideSource
两个函数,经过调用函数从而实现对目标函数实现细节的隐藏。
function foo() { /* ... */ }
console.assert(foo.toString().includes("..."));
foo.hideSource();
console.assert(foo.toString() === "function foo() { [ native code ] }");
复制代码
这种方式比指令方案要差一些:
和上面的方法相似,不过foo.hideSource()
返回的是一个被隐藏实现细节的函数,和上面的方法有相同的缺点,还有其余缘由:
toMethod()
方法的讨论,这是一个从ES2015开始的提案,该方法进行了一次函数克隆,但因为其复杂性而被TC39拒绝了。有人提出了这种方案,在源文件中先执行delete Function.prototype.toString
,可是这不适用于任何多领域环境,看下面的例子:
delete Function.prototype.toString;
function foo() { /* ... */ }
const otherGlobal = frames[0];
console.assert(otherGlobal.Function.prototype.toString.call(foo).includes("..."));
复制代码
并且这是一种很是钝的方式,只能在领域级别内使用,可能只有应用程序的开发人员使用。而指令的做用目标是库文件的开发者,应用级别的开发者可使用out-of-bound的解决方案(后面会提到)。
并且这个提案中的sensitive
指令未来可能会扩展,不只是delete Function.prototype.toString
隐藏源码这么简单的事情,考虑到该方案扩展性较差,也被否认了。
这种方法看起来很诱人,就像Symbol.isConcatSpreadable
或者Symbol.iterator
,可是它和第一种方案有相同的问题,并且仍是可逆的,彻底能够关闭。或者你想说咱们能够把它设计成true的时候隐藏实现细节,设置为false什么都不作,我以为这样会被diss。
function foo() { /* ... */ }
console.assert(foo.toString.includes("..."));
foo[Symbol.hideSource] = true;
console.assert(!foo.toString.includes("..."));
foo[Symbol.hideSource] = false;
console.assert(foo.toString.includes("...")); // oops
复制代码
不会,由于JavaScript已经有隐藏函数名和函数参数个数的机制,并且这个功能并非hide source
指令的目标需求,因此这个提案不会包含该行为。
function foo(a, b, c) { }
console.assert(foo.name === "foo");
console.assert(foo.length === 3);
// 经过defineProperty隐藏name和参数数量
Object.defineProperty(foo, "name", { value: "" });
Object.defineProperty(foo, "length", { value: 0 });
console.assert(foo.name === "");
console.assert(foo.length === 0);
复制代码
这个提案是针对JS的提案,只会影响JS代码的行为,devtools不考虑在内,这个提案也没有想过要改变devtools的行为。所以经过提案的指令被隐藏实现的函数,能够被任何具备特权的API审查到,好比devtools使用的API。
目前该提案只关心两件事:
下面这些是提案不考虑的:
console.log(function () {})
打印出来的内容console.log(new Error)
打印出来的内容这些都是和浏览器实现有关的,devtools团队只须要考虑用户体验,不用管JS的规范。
比较乐观的态度认为是不会的,hide soruce
不像 use strict
会让代码更好。hide source
对那些想要更高封装和自由度重构代码的做者来讲,是一种特殊的机制。
有开发者以为这两个指令能够节省内存,由于Function.prototype.toString
隐藏源码后,这部分代码能够直接删掉从而不占内存。sensitive
做用下Error.prototype.stack
能够省略隐藏了实现细节的函数,这样又会节省一部份内存,若是真的会节省内存,我想大部开发者都会选择使用指令吧,由于使用指令没什么坏处,还能优化性能,但实际上并无这么乐观,在最后面咱们会讨论这个问题。
preserve source
指令?咱们好像确实须要一个在使用hide source
的函数内部,经过使用preserve source
保留函数实现的细节,可是这种场景并很少,你能够把函数提取出来从而避免使用这个指令。可是对于直接调用eval
和反射类型的状况,咱们无法对文本作任何(能够提取出来的)假设,也就没法提取函数声明了。
TC39的历史中有不少想法、动机、提案关于节省内存,在2018年1月份的会议上,TC39的委员们意识到有两个关于这方面的提案:
第二种提案的背后考虑是由于,引擎为了让Function.prototype.toString
返回预期的结果,会占用大量内存保存源码和其全部的依赖项,若是存在一种方式让应用层开发者全局关闭源码存储,这样会节省大量内存。这种方式取决于应用运行的环境,若是是浏览器能够用meta标签,Node.js环境能够用flag。
2018 TC39的会议后,决定将这个想法分红两部分落实,其中一个就是这个提案,另外一个是根据宿主环境的out-of-bound方式,可是不久以后和引擎实现者讨论后发现,这套节省内存的机制前提是存在缺陷,对于那些保留源码进行懒编译的引擎来讲,用户的一个隐藏源码的指令并不会节省内存。
所以out-of-bound节省内存的开关到如今都没推动,若是浏览器引擎实现者改变了他们懒加载编译的技术,该提案附录将会更新并指导有兴趣作champion的同窗进行这方面的工做。