Function 們的 prototype 與 es6 class 語法
在 jawil 的 github issue 部落格 看到他探究 javascript 函數原型與物件原型的根源。 剛好之前自幹 promise 與 async function 時 對 AsyncFunction class 做了些研究,就 回了一篇心得 , 想不到回覆途中忽然想通是 es6 class extends 語法 讓子類別繼承靜態方法,造成子類別的原型直接指向父類別。
原回文
我是認為 js runtime 中物件就是有一個 __proto__
屬性,
在查詢屬性時就會一路用 proto 往上找。
函數可以當作建構函數,只是方便我們建立同類的物件;
並在建構物件時,把 proto 指向建構式的 prototype 屬性。
這些都只是包裝。 Object 與 Object.prototype 沒什麼特殊意義, 他只是提供了一些物件常用的方法, 而那些方法其實都能自己 patch 出來。 所以你大可自己寫一個 MyObject 建構式, 在 MyObject.prototype 上也可以自幹出 多數 Object.prototype 提供的方法。
唯一特例是 Function, Function.prototype 不是物件, 而也是函數;我不確定這有什麼意義。 但函數也是一種物件,可以有擁有屬性與原型, 所以 Function.prototype.__proto__ == Object.prototype。
關於 es6 新增的箭頭函數、generator、async function, 其中箭頭函數的原型也是指向 Function.prototype, 和一般函數沒有差別。
generator 與 async function
而 generator 與 async 就有趣了,
他們的建構函數不是公開的全域變數,
要用 (function*(){}).constructor
與 (async function() {}).constructor
來獲取。
(以下稱為 GeneratorFunction 與 AsyncFunction。)
這二個建構式也都和 Function 一樣能用來動態建構函數,
他們的實例的原型是各自建構子的 .prototype
,
*.prototype
的原型又指向 Function.prototype,
所以 instanceof 可以得到正確的結果。
但又一點很吊詭,GeneratorFunction 與 AsyncFunction
原型不是指向 Function.prototype。
原本 js 中所有函數包括建構函數的原型都是指向 Function.prototype,
但 GeneratorFunction 與 AsyncFunction 的原型是指向 Function。
GeneratorFunction.__proto__ == Function
。
es6 class extends 語法
後來我發現這可能是 es6 class 的緣故。
如果用 es6 class 語法繼承現有類別,
雖然子 constructor 仍是函數,
但 constructor.__proto__
會指向父建構式。
class SubObject extends Object {
}
SubObject.prototype instanceof Object.prototype // true
SubObject instanceof Object // true
SubObject.entries == Object.entries // true
目前想到的二個原因:
讓 instanceof 算符可以運作在 class 上,也就是建構函數上。 比較好看而已。
讓子類繼承父類的靜態方法。 因為原型指向了父類,自然能從子類存取到父類的屬性。 主要應該是這個原因,而且繼承時還能用 super 存取靜態方法。
所以 GeneratorFunction 與 AsyncFunction 在內部
應該是用類似 class extends Function
的方法實作的。
結論
我認為 js 中的函數與物件都是一個黑箱, 那些建構式只是其一個很有限的介面, 提供很少的方法讓我們接觸外部。
而函數又比物件更黑,
相比物件只是簡單的 key value 集合,
加上一個 __proto__
屬性。
基本上是可以自己實現的。
child.get = function (key) {
if (this.hasOwn(key)) return this.getOwn(key)
else if (this.parent) return this.parent.get(ket)
else return undefined // parent is null
但函數的閉包、this、yield await 都是透過黑魔法實現的功能, 需要在執行時做很多手腳才能辦到, 不然就是像 babel 要完全重編譯成另一種樣子。
回顧之前的經驗
今年早些做了點有趣的功能,像 自己實現 Promise 與 用 generator 實現 async 函數 , 所以研究了一下要怎麼為 generator function 加 method 但又不加到所有 function 上, 所以找到了 GeneratorFunction.prototype 這東西, 順便做了點實驗。
(結論是 async 可以透過 generator 實現,promise 可以以原生物件方式實現; 整個 async 與 promise 系列升級,只有 generator 是真正重要不可取代的元件。)
昨天才看到這篇,覺得也是另一種探討方向,
也想把自己的想法與發現寫下來,就寫了。
(發現 AsyncFunction.__proto__ != Function.prototype
。)
沒想到寫到一半忽然想通是 es6 class extends 的原因,
文章長度幾乎加倍……。
嘗試繼承 Function
上面提到,既然 async 與 generator function 都是繼承自 Function, 那可不可能自己繼承 Function 創造自己的 function 類別呢? 於是就自己實驗了。 如果不改寫 constructor 或在 constructor 中有呼叫 super 的話, 也就是不顯式返回 this 以外的物件, 那返回值就還是函數。
class MyFunction extends Function {
constructor() {
super()
this.name = 'my function'
}
}
typeof new MyFunction() // function
其實想一下就能理解,super 其實就是 Function, 而直接呼叫 Function 建構式,就是返回內容是傳入字串的函數, (上面是空函數。) extends 後在 constructor 內呼叫了 super, 其實和直接呼叫 Function 是沒有差別的, 所以想想返回值是函數也是理所當然的事。
雖然可以以此做簡單的客製化:
class AnotherFunction extends Function {
constructor(n) {
super('i', `return i + ${n}`)
}
}
let af = new AnotherFunction(3)
af(6) // 6 + 3 = 9
但這種可客製化的範圍太低了, 所有閉包都不能用,也不能用 this 存取自己的其它方法, 因為 function 執行時的 this 不是指向自己。 (可以用 arguments.callee 得到自己, 但還是很糟,因為 callee 廢棄了。)
有 google 到 一篇 stackoverflow 也在討論 extends Function 的可能性 , 做法有 JSON.stringify 傳入的 object 然後傳入 super, 應該也能 function.toString 之後傳入, 但都是要手動 parse,一樣不能用閉包。
然後有人提到 Proxy 中有個有趣的 apply handler,
可以代理函數被呼叫時的行為,
但好像只能用在要被代理的本來就是函數的情況。
不像 python 只要有 __call__
方法就被視為函數。
如果能繼承函數,想試著實作一個 function hook, 讓 function 可以有 after before 之類的 hook。 看來可能要用 proxy 實現,但用 proxy 又不如用物件, 讓各 hook 與附加、移除 hook 的方法能用物件的屬性暴露。 把包裝好的函數用物件的方法暴露。 所以 proxy 的地位其實有點尷尬。