這篇文章是對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的同窗進行這方面的工做。