JavaScriptのprototypeを理解したい
ExtendScriptを触り始め、既存のクラスを拡張したい欲求に駆られたものの上手くいかなかった。
上手くいかなかったが C.prototype.method = () => ...
の古きクラス的表現のイディオムに久しぶりに触れたことで、かつて class
の登場によって理解する必要がなくなったプロトタイプチェーンの理解に再び意欲が湧いた。class
が結局はプロトタイプチェーンによるクラス表現の糖衣構文だという知識はあったので、理解したとして無駄にはならないだろうという思いもあった。
名前が非常に紛らわしいですが、 prototype はオブジェクトのプロトタイプを表すプロパティではありません。 prototype プロパティは「そのオブジェクトがコンストラクタとして利用された際に作成される新しいオブジェクト」のプロトタイプを決めるものです。 オブジェクトのプロトタイプを表すプロパティは proto あるいは言語仕様書で [[Prototype]] と表されるもので prototype プロパティとは異なります。 ここを勘違いしてしまうと混乱のもとになるので自分で図を書いたりコードを実行してよく違いを理解しておいて下さい。
これが値千金の説明であり、prototypeの全て。もしくはわかりづらさの全て。
プロトタイプチェーン自体はオブジェクト同士で行うことができる。オブジェクトのプロトタイプに別のオブジェクトを指定する。
base = { fnB: () => 'fnB defined on base object' }
// { fnB: [Function: fnB] }
base.fnB()
// 'fnB defined on base object'
sub = {}
// {}
sub.fnB()
// Uncaught TypeError: sub.fnB is not a function
Object.setPrototypeOf(sub, base)
// {}
sub.fnB()
// 'fnB defined on base object'
プロトタイプを指定するための以下の表現は全て等価。よく見る .__proto__
は廃止予定らしい。プロトタイプを指定する操作には .prototype
は現れないのを覚えておこう。これは new
をするときに、新しいオブジェクトの .proto
として参照される。
Object.setPrototypeOf(sub, base)
sub.proto = base
sub.__proto__ = base // ←Deprecated!
ただ、Node.js v16.13.0 で確認すると .proto
はプロトタイプを指していないらしい。
Object.getPrototypeOf(sub) === sub.__proto__
// true
Object.getPrototypeOf(sub) === sub.proto
// false
sub.proto === sub.__proto__
// false
振る舞いがよくわからないので、ここでは .__proto__
を使うことにしよう。
プロトタイプを指定する操作には .prototype は現れないのを覚えておこう。これは new をするときに、新しいオブジェクトの .proto として参照される。
を確認する。
F = function () {}
// [Function: F]
F.prototype.methodF = () => 'methodF defined on F.prototype'
// [Function (anonymous)]
f = new F()
// F {}
f.__proto__ === F.prototype
// true
f.methodF()
// 'methodF defined on F.prototype'
確かに f.__proto__ === F.prototype
になっている。
function
による関数もアロー表記 =>
によるアロー関数も共に Function
のインスタンスだが、アロー関数には .prototype
が存在しない。
f_function = function () {}
// [Function: f_function]
f_function instanceof Function
// true
f_function.prototype
// {}
f_function.prototype.method = () => 'my-method'
// [Function (anonymous)]
f_arrow = () => {}
// [Function: f_arrow]
f_arrow instanceof Function
// true
f_arrow.prototype
// undefined
f_arrow.prototype.method = () => 'my-method'
// Uncaught TypeError: Cannot set properties of undefined (setting 'method')
クラスとは何かを考えると「メソッドは共有したいけれどフィールドは共有したくない(インスタンスごとに持ちたい)もの」とも言える。そのメソッドのみを共有するレイヤーとして instanceof Sub → prototype → inistance of Base → prototype という一見婉曲的とも言えるプロトタイプチェーンの prototype というレイヤーがあるということなのだろう。
もしフィールドの値も共有しても問題ないものであれば、単純にオブジェクト間に直接プロトタイプの関係をつければいい。
new Object
できるということは Object
は Function
でもあるということか。
Object instanceof Function
// true
Function instanceof Object
// true
どうやらそうらしい。この関係は唯一無二なのではないか。
JavaScriptを始めた人なら誰でも思うであろう「Object.keys
がインスタンスから生えていたらいいのに」はこれで叶えられる。
Object.prototype.getKeys = function () { return Object.keys(this) }
// [Function (anonymous)]
a = { a: 12, b: 24 }
// { a: 12, b: 24 }
a.getKeys()
// [ 'a', 'b' ]
やって嬉しいかは知らない。多分やらない方がいいんだろうが、具体的に何故かはわからない。ちなみにアロー関数だと globalThis
がbindされてしまうので期待する動作にならない。
Object.prototype.getKeys = () => Object.keys(this)
// [Function (anonymous)]
a.getKeys()
// [
// 'global',
// 'clearInterval',
// 'clearTimeout',
// 'setInterval',
// 'setTimeout',
// 'queueMicrotask',
// 'performance',
// 'clearImmediate',
// 'setImmediate',
// 'a'
// ]
Function instanceof Object
// true
Object instanceof Function
// true
の関係が気になったので、プロトタイプチェーン上の直接の参照関係を見る。
Object.getPrototypeOf(Function) === Object.prototype
// false
Object.getPrototypeOf(Function) === Function.prototype
// true
Object.getPrototypeOf(Object) === Function.prototype
// true
Object.getPrototypeOf(Object) === Object.prototype
// false
つまり不正確を承知で書くなら Function = new Function()
、Object = new Function()
という関係になる。
Object instanceof Function
// true ←これは分かった
Function instanceof Object
// true ←これはどうなっている?
これが気になったのでもう少し調べてみる。
Function.prototype
// {}
Object.getPrototypeOf(Function.prototype) === Object.prototype
// true
なのでつまりこういうことか。Object.getPrototypeOf
が長かったので .proto
で表記した。
.proto
を追っていけば Function instanceof Object
であることがわかる。
instanceof
はプロトタイプチェーンを辿るが、プロトタイプチェーン上の直接の new
元相当を得るためのヘルパー関数 directInstanceOf
を定義する。
directInstanceOf = (o, C) => Object.getPrototypeOf(o) === C.prototype
// [Function: directInstanceOf]
class Base {}
class Sub extends Base {}
sub = new Sub()
base = new Base()
[directInstanceOf(sub, Sub), sub instanceof Sub]
// [ true, true ]
[directInstanceOf(sub, Base), sub instanceof Base]
// [ false, true ]
[directInstanceOf(sub, Object), sub instanceof Object]
// [ false, true ]
directInstanceOf(Function, Function)
// true
directInstanceOf(Object, Function)
// true
プロトタイプチェーンを取得する関数を書いた。
getPrototypeChain = (o, knownClasses = []) => {
const chain = []
const nullPrototype = Object.getPrototypeOf({})
do {
o = Object.getPrototypeOf(o)
const p = knownClasses.find(c => c.prototype === o)
chain.push(p ? `${p.name}.prototype` : o.toString())
} while (o !== nullPrototype)
return chain
}
class Base {}
class Sub extends Base {}
base = new Base()
sub = new Sub()
getPrototypeChain(sub, [Sub, Base, Function, Object])
// [ 'Sub.prototype', 'Base.prototype', 'Object.prototype' ]
getPrototypeChain(Function, [Function, Object])
// [ 'Function.prototype', 'Object.prototype' ]
getPrototypeChain(Object, [Function, Object])
// [ 'Function.prototype', 'Object.prototype' ]
getPrototypeChain(() => {}, [Function, Object])
// [ 'Function.prototype', 'Object.prototype' ]
getPrototypeChain({}, [Function, Object])
// [ 'Object.prototype' ]
Function
と Object
の関係が分かりづらいのはなんだ。結局 Function.prototype
に {} instanceof Object
がセットされているからか。
null prototypeってなんだろう。
これか。
o = Object.create()
// Uncaught TypeError: Object prototype may only be an Object or null: undefined
// at Function.create (<anonymous>)
o = Object.create(null)
// [Object: null prototype] {}
o.toString()
// Uncaught TypeError: o.toString is not a function
Object.create
を使うとプロトタイプを指定して Object インスタンスを作成できる。
base = { fn: () => 'this is base' }
// { fn: [Function: fn] }
o = Object.create(base)
// {}
Object.getPrototypeOf(o) === base
// true
base
自体は instanceof Object
なので Object.prototype
のメソッドを利用できる。
base.toString()
// '[object Object]'
o.toString()
// '[object Object]'
プロトタイプに null を指定してやると Object.prototype
のメソッドにアクセスできなくなる。しかし __proto__
が undefined
になるのがよくわからない。
o = Object.create(null)
// [Object: null prototype] {}
o.toString()
// Uncaught TypeError: o.toString is not a function
Object.getPrototypeOf(o)
// null
Object.getPrototypeOf(o) === null
// true
o.__proto__
// undefined ←?
Object.setPrototypeOf
で後から null
を指定することもできる。
o = {}
// {}
Object.getPrototypeOf(o)
// [Object: null prototype] {}
Object.setPrototypeOf(o, null)
// [Object: null prototype] {}
o.toString()
// Uncaught TypeError: o.toString is not a function
o0 = Object.create(null)
// [Object: null prototype] {}
Object.getPrototypeOf(o0)
// null
o = {}
// {}
Object.getPrototypeOf(o)
// [Object: null prototype] {}
Object.getPrototypeOf(Object.getPrototypeOf(o))
// null
Object.prototype
// [Object: null prototype] {}
Object.getPrototypeOf(o) === Object.prototype
// true
そうか、Object
のインスタンスもまたプロトタイプを Object
として持つからややこしいのか。
つまり [Object: null prototype]
はそれ以上プロトタイプチェーンを辿れない Object
インスタンスだよという意味か。あくまでそのインスタンスのプロトタイプが null
なだけでObject
インスタンス自体は存在するので、フィールドを設定することができる。
Object.prototype
// [Object: null prototype] {}
Object.prototype.myField = 'myField on Object.prototype'
// 'myField on Object.prototype'
o = {}
o.myField
// 'myField on Object.prototype'
Object.prototype.toString
を消してみる。
Object.prototype = {}
// {}
o.toString()
// Uncaught ReferenceError: o is not defined
o = {}
// {}
o.toString()
// '[object Object]'
delete Object.prototype.toString
// true
o.toString()
// Uncaught TypeError: o.toString is not a function
Object.getOwnPropertyNames
はこういう用途だと面白いメソッドだな。
Object.getOwnPropertyNames(Object)
// [
// 'length',
// 'name',
// 'prototype',
// 'assign',
// 'getOwnPropertyDescriptor',
// 'getOwnPropertyDescriptors',
// 'getOwnPropertyNames',
// 'getOwnPropertySymbols',
// 'is',
// 'preventExtensions',
// 'seal',
// 'create',
// 'defineProperties',
// 'defineProperty',
// 'freeze',
// 'getPrototypeOf',
// 'setPrototypeOf',
// 'isExtensible',
// 'isFrozen',
// 'isSealed',
// 'keys',
// 'entries',
// 'fromEntries',
// 'values',
// 'hasOwn'
// ]
Object.getOwnPropertyNames(Object.prototype)
// [
// 'constructor',
// '__defineGetter__',
// '__defineSetter__',
// 'hasOwnProperty',
// '__lookupGetter__',
// '__lookupSetter__',
// 'isPrototypeOf',
// 'propertyIsEnumerable',
// 'toString',
// 'valueOf',
// '__proto__',
// 'toLocaleString'
// ]
Object.prototype
は上書きできないということか。
Object.prototype = { myMethod: () => 'my-method' }
// { myMethod: [Function: myMethod] }
Object.prototype.myMethod()
// Uncaught TypeError: Object.prototype.myMethod is not a function
prototypeそこそこ理解した。
REPL環境からのコピペ用スニペット。
>
>
> f = () => {
...
... return 'hello'
... }
[Function: f]
>
>
のようなREPLの出力を実行可能なJavaScriptに変換するワンライナー。
bash-3.2$ cat <<'EOF' | sed -Ee 's%^%// %g' -e 's%// (>|\.\.\.)( |$)%%g' | grep -E .
> >
> >
> >
> > f = () => {
> ...
> ... return 'hello'
> ... }
> [Function: f]
> >
> >
> EOF
f = () => {
return 'hello'
}
// [Function: f]
bash-3.2$
面倒だからコピペ時に変換できるようにしたい。
sed -Ee 's%^%// %g' -e 's%// (>|\.\.\.)( |$)%%g' | grep -E . | pbcopy
をmacOSのShortcutsに仕込む。Copy to Clipboard でやろうとしたらクリップボードにはコピーできるけれどそこからペーストできない現象に当たったので pbcopy
を使った。
クリップボードにコピーできることの確認はFinder→Show clipboardで行った。
python版はREPLがこうなので
>>> async def say_after(delay: int, what: str):
... await asyncio.sleep(delay)
... print(what)
...
>>> say_after(1, 'test')
<coroutine object say_after at 0x10cfd8d60>
>>>
こうなる。
sed -Ee 's%^%# %g' -e 's%# (>>>|\.\.\.)( |$)%%g' | grep -E .
VSCodeのintegrated terminalじゃ使えなかった。