logo logo_atte 日記 随筆 何処
JavaScript覚書
1 関数とプロパティ継承
1.1 ネイティブのコンストラクタ関数
ネイティブのコンストラクタ関数は、newキーワードをつけてもつけなくても同じ。 (ただし、プリミテブィブ値のコンストラクタ関数は除く。)
  1. var foo = Function("x", "y", "return x+y");
  2. var bar = new Function("x", "y", "return x+y");
  3. var baz = new foo;
  4. var qux = new foo();
  5. console.log(foo); // 関数型を返す
  6. console.log(bar); // 関数型を返す(すぐ上に同じ)
  7. console.log(baz); // 空オブジェクト(未設定のthis)を返す(すぐ上とは異なる)
  8. console.log(qux); // コンストラクタ関数の括弧の有無は問わない(すぐ上に同じ)
Arrayでも同様。下記で出力結果は変わらない。
  1. var foo = Array(1, 2, 3);
  2. var bar = new Array(1, 2, 3)
  3. console.log('===foo===')
  4. console.log("foo", foo);
  5. console.log("foo.prototype", foo.prototype);
  6. console.log("foo.constructor", foo.constructor);
  7. console.log('===bar===')
  8. console.log("bar:", bar);
  9. console.log("bar.prototype:", bar.prototype);
  10. console.log("bar.constructor:", bar.constructor);
次項の結果からネイティブのコンストラクタ関数はObjectを返していると推測する。 (thisは無視されている。)
1.2 ユーザ定義のコンストラクタ関数における暗黙の引数(this)と戻り値
コンストラクタ関数(newキーワードつき)は、呼出時に新たに生成されたObjectインスタンスをthisに設定する。コンストラクタ関数(newキーワードつき)の戻り値は、返す式がundefinedを含むプリミテブィブ値であればthisになる。返す式がオブジェクトであればそのままである。
  1. var foo = function () {
  2. this.foo = "FOO";
  3. return "FOO";
  4. };
  5. var bar = function () {
  6. this.bar = "BAR";
  7. return { baz: "BAZ" };
  8. };
  9. console.log(new foo()); // 文字列ではなくObject(thisである)が返る
  10. console.log(new bar()); // returnの式であるObject(thisではない)が返る
1.3 プロパティチェーン
コンストラクタ関数が定義された場合(呼ばれた場合ではない)[1]、作成された関数にはObject型のprototypeプロパティが付加され、 prototypeプロパティはその内部にconstructorプロパティを持つ。このprototypeプロパティ内のconstructorプロパティに、定義している関数そのものへの参照が格納されている。関数自身が直接constructorプロパティを持つわけではないことに注意せよ。
  • var Foo = function Foo() {};
  • --> Foo.prototype: { constructer: Foo() }
  • --> Foo.hasOwnProperty('constructor'): false
ただし、ユーザ定義のコンストラクタ関数は、ネイティブのFunctionコンストラクタ関数から作成されるため、 Functionコンストラクタ関数のprototypeプロパティにチェーンする。
  • Foo.__proto__: Function.prototype
また、Functionコンストラクタ関数のprototypeプロパティは無名の内部関数であり、この無名関数のconstructorプロパティは、Functionコンストラクタ関数を指している。
  • Function.prototype: (anonymous internal function)
  • (anonymous internal function).constructor: Function
よって、結果としてユーザ定義のコンストラクタ関数のconstructorプロパティは、 (ネイティブのFunctionコンストラクタ関数のprototypeプロパティ経由で、) ネイティブのFunctionコンストラクタ関数を参照する。 'var foo = new Foo()'も含めて図にすれば以下である。
figure
Functionコンストラクタ関数では'__proto__'プロパティが循環リンクとなる。
プロパティチェーンで本質的なのは'__proto__'プロパティである。ただし、'__proto__'プロパティは、内部プロパティあるいは疑似プロパティであるため、 hasOwnPropertyで存在を確認できない。

  1. [1] 定義時と言うのも正しくはない。厳密にはFunctionインスタンスが生成された時である。
1.3.1 開眼!JavaScript
開眼!JavaScript』の8.8で一日悩んだので覚書。なぜプロパティチェーンが起きないのかから始まる。
  1. var foo = new function Foo(X) {
  2. return function Bar(Y) {
  3. return function Baz(Z) {
  4. return Z;
  5. };
  6. };
  7. };
  8. var bar = new foo()
  9. var baz = new bar()
fooもbarもコンストラクタ関数がFunctionになる。なぜ、fooのコンストラクタ関数がFooで、barのコンストラクタ関数がBarにならないのか? bazだけは正しくコンストラクタ関数がBazになっている。これは分解してみれば解決。
  1. var Baz = function Baz(Z) { return Z; };
  2. var Bar = function Bar(Y) { return Baz; };
  3. var Foo = function Foo(X) { return Bar; };
  4. var foo = new Foo(); // == Bar
  5. var bar = new foo(); // == new Bar() == Baz
  6. var baz = new bar(); // == new Baz() == {} <-- ZがundefinedであるからObject(this)になる
fooはBar関数で、barはBaz関数であるから,そのコンストラクタ関数は当然Functionだ。 bazのみObjectとなる。じゃ、どうやってプロパティチェーンを起こせばいんだ?
  1. var Foo = function Foo() { this.foo = "foo" };
  2. Foo.prototype.pFoo = "pFoo";
  3. var Bar = function Bar() { this.bar = "bar" };
  4. Bar.prototype.pBar = "pBar";
  5. var Baz = function Baz() { this.baz = "baz" };
  6. Baz.prototype.pBaz = "pBaz";
  7. var baz = new Baz();
Bazの親をBar、Barの親をFooにするためにどうすればいいのかで四苦八苦。 bazに"pBaz"のみでなく、"pBar"や"pFoo"を継承させるにはどうすればいいか?結論としては以下。
  1. var Foo = function Foo() { this.foo = "foo" };
  2. Foo.prototype.pFoo = "pFoo";
  3. var Bar = function Bar() { this.bar = "bar" };
  4. Bar.prototype.pBar = "pBar";
  5. Bar.prototype.__proto__ = Foo.prototype;
  6. var Baz = function Baz() { this.baz = "baz" };
  7. Baz.prototype.pBaz = "pBaz";
  8. Baz.prototype.__proto__ = Bar.prototype;
  9. var baz = new Baz();
Sample of Prototype Chain
わかってしまえば当たり前なんだけど、当初、prototypeとconstructorだけでどうにかしたい。 (__proto__は正式なプロパティではない。) 加えて、Foo,Bar,Baz,bazが、 constructorプロパティを持っていると誤解していたため大混乱となった。 [2] 8章を図表の細部や訳注まで含めて詳細に読んでいれば誤解はなかったはずだが、早い段階(開眼!...1.2)での次の表現を、
  1. var Person = function Person(...) { ... };
  2. var cody = new Person(...);
  3. console.log(cody.constructor); // Person関数そのものを出力
コンスタラクタ関数のインスタンスは、「元のコンスタラクタ関数への参照を(直接)constructorプロパティに持つ[3]」と、思い込んでいたことが問題であった。

  1. [2] Foo,Bar,Bazのprototypeプロパティがconstructorを持っている。
  2. [3] 正しくは、元のコンスタラクタ関数への参照を、コンスタラクタ関数のprototypeプロパティ内のconstructorプロパティが持っている。よって、インスタンスは、自身にconstructorプロパティは存在しないものの、プロトタイプチェーンによってコンスタラクタ関数が持つprototypeプロパティ内のconstructorプロパティにアクセスできる。
1.3.2 Luaとの対比
関数が第一級関数(1st-class-object)であることからLuaに似ている。プロトタイプベースのオブジェクト指向プログラミング言語なんて言う特徴は、ほぼこの第一級関数のサポートから派生しているだけにすぎない。というわけで、前記'Sample of Prototype chaine'をLuaで書きなおして対比すると、理解が深まる、、、ような気がする。
  1. Foo = { pFoo = "pFoo" }
  2. setmetatable(Foo, {
  3. __call = function (f)
  4. return setmetatable({ foo = "foo" }, { __index = Foo })
  5. end,
  6. })
  7. Bar = { pBar = "pBar" }
  8. setmetatable(Bar, {
  9. __call = function (f)
  10. return setmetatable({ bar = "bar" }, { __index = Bar })
  11. end,
  12. __index = Foo,
  13. })
  14. Baz = { pBaz = "pBaz" }
  15. setmetatable(Baz, {
  16. __call = function (f)
  17. return setmetatable({ baz = "baz" }, { __index = Baz })
  18. end,
  19. __index = Bar,
  20. })
  21. baz = Baz()
  22. print(baz.pFoo) -- "pFoo"
開眼!...では次の形式でのプロトタイプチェーンの実装の例もある(開眼!...8.12)。
  1. var Foo = function Foo() { this.foo = "foo" };
  2. Foo.prototype.pFoo = "pFoo";
  3. var Bar = function Bar() { this.bar = "bar" };
  4. Bar.prototype.pBar = "pBar";
  5. Bar.prototype = new Foo();
  6. var Baz = function Baz() { this.baz = "baz" };
  7. Baz.prototype.pBaz = "pBaz";
  8. Baz.prototype = new Bar();
  9. var baz = new Baz();
Sample of Prototype Chain 2
prototypeプロパティを書き替えることから、この場合は当然"pBaz"や"pBar"にはアクセスできない。 (ちなみに最初のサンプルでは"foo"や"bar"にアクセスできない。) これをLuaで実装するなら以下である。
  1. Foo = { pFoo = "pFoo" }
  2. setmetatable(Foo, {
  3. __call = function (f)
  4. return setmetatable({ foo = "foo" }, { __index = Foo })
  5. end,
  6. })
  7. Bar = { pBar = "pBar" }
  8. setmetatable(Bar, {
  9. __call = function (f)
  10. return setmetatable({ bar = "bar" }, { __index = Foo() })
  11. end,
  12. })
  13. Baz = { pBaz = "pBaz" }
  14. setmetatable(Baz, {
  15. __call = function (f)
  16. return setmetatable({ baz = "baz" }, { __index = Bar() })
  17. end,
  18. })
  19. baz = Baz()
  20. print(baz.pFoo) -- "pFoo"
以上の例ではJavaScriptのFooとFoo.prototypeを、Lua側ではFooにまとめてしまっている。厳密に再実装するならprototypeプロパティ部分を分離しないといけない。
  1. Foo = { prototype = { pFoo = "pFoo" } }
  2. setmetatable(Foo, {
  3. __call = function (f)
  4. return setmetatable({ foo = "foo" }, { __index = Foo.prototype })
  5. end,
  6. })
  7. Bar = { prototype = { pBar = "pBar" } }
  8. setmetatable(Bar, {
  9. __call = function (f)
  10. return setmetatable({ bar = "bar" }, { __index = Bar.prototype })
  11. end,
  12. })
  13. setmetatable(Bar.prototype, { __index = Foo() })
  14. -- or setmetatable(Bar.prototype, { __index = Foo.prototype })
  15. -- etc...
大体においてJavaScriptの__proto__を、 Luaのメタテーブルの__indexと読みかえれば同じこと。