Closed28

JavaScriptのprototypeを理解したい

astkastk

ExtendScriptを触り始め、既存のクラスを拡張したい欲求に駆られたものの上手くいかなかった。

https://zenn.dev/link/comments/7ca61929310980

上手くいかなかったが C.prototype.method = () => ... の古きクラス的表現のイディオムに久しぶりに触れたことで、かつて class の登場によって理解する必要がなくなったプロトタイプチェーンの理解に再び意欲が湧いた。class が結局はプロトタイプチェーンによるクラス表現の糖衣構文だという知識はあったので、理解したとして無駄にはならないだろうという思いもあった。

astkastk

https://www.yunabe.jp/docs/javascript_class_in_google.html

名前が非常に紛らわしいですが、 prototype はオブジェクトのプロトタイプを表すプロパティではありません。 prototype プロパティは「そのオブジェクトがコンストラクタとして利用された際に作成される新しいオブジェクト」のプロトタイプを決めるものです。 オブジェクトのプロトタイプを表すプロパティは proto あるいは言語仕様書で [[Prototype]] と表されるもので prototype プロパティとは異なります。 ここを勘違いしてしまうと混乱のもとになるので自分で図を書いたりコードを実行してよく違いを理解しておいて下さい。

これが値千金の説明であり、prototypeの全て。もしくはわかりづらさの全て。

astkastk

プロトタイプチェーン自体はオブジェクト同士で行うことができる。オブジェクトのプロトタイプに別のオブジェクトを指定する。

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__ を使うことにしよう。

astkastk

プロトタイプを指定する操作には .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 になっている。

astkastk

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')
astkastk

クラスとは何かを考えると「メソッドは共有したいけれどフィールドは共有したくない(インスタンスごとに持ちたい)もの」とも言える。そのメソッドのみを共有するレイヤーとして instanceof Sub → prototype → inistance of Base → prototype という一見婉曲的とも言えるプロトタイプチェーンの prototype というレイヤーがあるということなのだろう。

もしフィールドの値も共有しても問題ないものであれば、単純にオブジェクト間に直接プロトタイプの関係をつければいい。

astkastk

new Object できるということは ObjectFunction でもあるということか。

Object instanceof Function
// true
Function instanceof Object
// true

どうやらそうらしい。この関係は唯一無二なのではないか。

astkastk

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'
// ]
astkastk
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() という関係になる。

astkastk
Object instanceof Function
// true ←これは分かった
Function instanceof Object
// true ←これはどうなっている?

これが気になったのでもう少し調べてみる。

Function.prototype
// {}
Object.getPrototypeOf(Function.prototype) === Object.prototype
// true

なのでつまりこういうことか。Object.getPrototypeOf が長かったので .proto で表記した。

.proto を追っていけば Function instanceof Object であることがわかる。

astkastk

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
astkastk

プロトタイプチェーンを取得する関数を書いた。

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' ]
astkastk

FunctionObject の関係が分かりづらいのはなんだ。結局 Function.prototype{} instanceof Object がセットされているからか。

astkastk

これか。

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
astkastk
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 として持つからややこしいのか。

astkastk

つまり [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'
astkastk

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
astkastk

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'
// ]
astkastk

Object.prototype は上書きできないということか。

Object.prototype = { myMethod: () => 'my-method' }
// { myMethod: [Function: myMethod] }
Object.prototype.myMethod()
// Uncaught TypeError: Object.prototype.myMethod is not a function
astkastk

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$
astkastk

面倒だからコピペ時に変換できるようにしたい。

astkastk
sed -Ee 's%^%// %g' -e 's%// (>|\.\.\.)( |$)%%g' | grep -E . | pbcopy

をmacOSのShortcutsに仕込む。Copy to Clipboard でやろうとしたらクリップボードにはコピーできるけれどそこからペーストできない現象に当たったので pbcopy を使った。

astkastk

クリップボードにコピーできることの確認はFinder→Show clipboardで行った。

astkastk

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 .
astkastk

VSCodeのintegrated terminalじゃ使えなかった。

このスクラップは2022/01/03にクローズされました