【提案】function implementation hiding

  • status: stage-2
  • repo

这篇文章是对function-implementation-hiding提案现状的归纳,在12月TC39会议上,它可能成为stage-3的提案,成为stage-3的提案修改为本会很是高,但愿能引发更多开发者思考,有关心的问题尽早提出来,让JS变得更好。javascript

简介

函数实现隐藏,这个提案在目前已有的指令use strict基础上,增长两个新指令hide sourcesensitive(这两个名字是暂时的,在这个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 sourceFunction.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)
复制代码

使用sensitiveFunction.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 sourcesensitive最终做用在Error.prototype.stack的效果可能和上面展现的并不同,关于这方面的讨论请看#33bash

解决什么问题?

Function.prototype.toString

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()

Error.prototype.stack

JavaScript的(事实上是非标准的)Error.prototype.stack getter 展现了存在或不存在堆栈信息状况下的调用行为。对于递归调用函数,会在堆栈展现递归调用的次数。若是调用行为依赖了某些秘密的值,不管是在源码层面仍是词法层面都会致使这个秘密的值被部分或者所有暴露,另外它还会带出一些文件属性的位置信息,和 toString以相似的方式影响着重构。

解决方法

解决方正文开始就谈到了,使用hide source或者sensitive指令,改变函数toString()的输出从而阻止其暴露出具体的实现细节,这两个指令像use strict同样能够用在整个文件或者某个函数中,一样是向下做用,从而使其做用域内的全部内容以及在函数做用域内的函数自己都被隐藏实现细节,举个例子:

function foo() {
  const x = () => {};

  const y = () => {
    "hide source";
    class Z {
      m() {}
    }
  };
}
复制代码

在这个例子中foox都不会被隐藏实现细节,而yZ以及Z.prototype.m都会被隐藏实现细节。

为何选择使用指令实现?

该提议很大程度上借鉴了JavaScript现有的指令支持的优点

  1. 能够简单的从文件级隐藏实现细节,经过在文件开头加hide source或者sensitive
  2. 能够用匿名函数包裹,从而将非隐藏实现细节和隐藏实现细节的代码绑定在一块儿。
  3. 向后兼容,能够轻松地部署代码,从而尽力达到隐藏实现细节的目的,在未实现此提案的引擎中,指令不生效。
  4. 方便工具简单、静态的决定一个函数是否应该被隐藏实现细节。

那些被拒绝掉的方案

A one-time hiding function

这个方案引入了Error.hideFromStackTracesFunction.prototype.hideSource两个函数,经过调用函数从而实现对目标函数实现细节的隐藏。

function foo() { /* ... */ }

console.assert(foo.toString().includes("..."));

foo.hideSource();

console.assert(foo.toString() === "function foo() { [ native code ] }");
复制代码

这种方式比指令方案要差一些:

  • 很难作到一次性隐藏全部函数的实现细节,指令能够作到文件级隐藏
  • 能够隐藏全部人的函数,并不仅是你本身建立和控制的函数
  • 非词法的,一些做用在源代码上的工具要依赖启发式的技术来决定函数的实现是否应该被隐藏
  • 这个属性做用在函数上而不是整个源代码上,抽象级别有错误,而且很难推理。

A clone-creating hiding function

和上面的方法相似,不过foo.hideSource()返回的是一个被隐藏实现细节的函数,和上面的方法有相同的缺点,还有其余缘由:

  • 克隆函数难以说明和解释,能够看看关于toMethod()方法的讨论,这是一个从ES2015开始的提案,该方法进行了一次函数克隆,但因为其复杂性而被TC39拒绝了。
  • 一些函数很难被克隆函数彻底替代,这些方法的执行须要依赖他们正确的上下文环境。

delete Function.prototype.toString

有人提出了这种方案,在源文件中先执行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做为开关

这种方法看起来很诱人,就像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);
复制代码

对devtools和其余审查函数实现的方法有什么影响?

这个提案是针对JS的提案,只会影响JS代码的行为,devtools不考虑在内,这个提案也没有想过要改变devtools的行为。所以经过提案的指令被隐藏实现的函数,能够被任何具备特权的API审查到,好比devtools使用的API。

目前该提案只关心两件事:

  1. Function.prototype.toString() 输出的源码字符串
  2. Error.prototype.stack 输出的错误栈信息

下面这些是提案不考虑的:

  1. 使用console.log(function () {})打印出来的内容
  2. 使用console.log(new Error)打印出来的内容
  3. unhandleed exception 或 unhandleed rejection输出的结果
  4. 使用devtools看到源码或者HTTP响应的内容
  5. 在devtools上断点调试或者在函数中因uncaught exception暂停的信息
  6. 其余...

这些都是和浏览器实现有关的,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的委员们意识到有两个关于这方面的提案:

  1. 一种封装机制,与源代码一块儿in-bound使用,特别适合于库。(这个提案)
  2. 一种节省内存机制,out-of-bound源代码使用,特别适合应用。

第二种提案的背后考虑是由于,引擎为了让Function.prototype.toString返回预期的结果,会占用大量内存保存源码和其全部的依赖项,若是存在一种方式让应用层开发者全局关闭源码存储,这样会节省大量内存。这种方式取决于应用运行的环境,若是是浏览器能够用meta标签,Node.js环境能够用flag。

2018 TC39的会议后,决定将这个想法分红两部分落实,其中一个就是这个提案,另外一个是根据宿主环境的out-of-bound方式,可是不久以后和引擎实现者讨论后发现,这套节省内存的机制前提是存在缺陷,对于那些保留源码进行懒编译的引擎来讲,用户的一个隐藏源码的指令并不会节省内存。

所以out-of-bound节省内存的开关到如今都没推动,若是浏览器引擎实现者改变了他们懒加载编译的技术,该提案附录将会更新并指导有兴趣作champion的同窗进行这方面的工做。

相关文章
相关标签/搜索