【提案】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的同窗進行這方面的工做。

相關文章
相關標籤/搜索