JavaScriptのオブジェクトシステム

(2001.10.11) 新規作成。

# 概要

ECMAScript / JavaScriptは,プロトタイプベースな (prototype-based) オブジェクト指向言語。Java、Rubyなどメジャーなプログラミング言語は、プロトタイプベースではなく、クラスベース (class-based)。

プロトタイプベースのプログラミング言語には、例えば, 次がある.

クラスベースのオブジェクト指向言語では、まずオブジェクト (インスタンス) の雛型となるクラスを定義する。クラスにプロパティ (メンバ変数)、メソッドなどを定義する。オブジェクトはクラスから生成する。クラスが同じであれば, それぞれのオブジェクトは同じメソッドを持つ。

JavaScriptでは,クラスというものがなく、それぞれのオブジェクトにメソッド、プロパティを定義する。また、JavaScriptのオブジェクトはただのハッシュで、(プロパティ名: プロパティ値) の集まり。プロパティ値が関数であれば, それがメソッドになる。

# オブジェクトの生成

JavaScriptでは、オブジェクトはnew演算子で生成する。

関数をコンストラクタと見立てる. この関数の名前 (nameプロパティ) がクラス名になる.

  new 関数オブジェクト ( 引数 )

次の例の Hoge() はただの関数。これを引数として new 演算子を実行すると, 次のような動作を行う;

  1. new 演算子の対象F をコンストラクタとする.
  2. コンストラクタが Object でない場合, TypeError を投げる.
  3. コンストラクタの [[Construct]] 内部メソッドを呼び出す. [[Construct]] 内部メソッドは、次のように動作する;
    1. 空のオブジェクトを生成する
    2. [[Class]] 内部プロパティを "Object" とする
    3. [[Prototype]] 内部プロパティとして, 次を設定する;
      • コンストラクタが prototypeプロパティを持ち, それが Object である場合, それ.
      • それ以外の場合, 標準組み込み Objectprototype プロパティ
    4. コンストラクタを呼び出す; この際に, this は 1. で生成したオブジェクトを指す。引数は new演算子に与えたもの.
    5. コンストラクタの戻り値が Object ならその戻り値を、それ以外の場合は, 1. のオブジェクトを返す。

[[Prototype]] 内部プロパティと、prototypeプロパティとは別物。

コンストラクタのprototype プロパティにメソッドを設定することで、新しく生成されたオブジェクトのメソッドになる。

JavaScript
  1. function Hoge() {
  2. this.foo = 10;
  3. }
  4. Hoge.prototype = {
  5. bar: function() {
  6. return 'bar called';
  7. }
  8. }
  9. var o1 = new Hoge();
  10. document.write( o1.foo ); // 10
  11. document.write( o1.bar() ); // "bar called"

コンストラクタが何を返すかで, 生成されるオブジェクトが変わるのが微妙。nullはプリミティブ値なので、コンストラクタがnullを返したときは新たに生成されたオブジェクトがnew演算子の値になる。下手にオブジェクトを返してしまうと、それになってしまう。

JavaScript
  1. function C() { this.p = 100; return new String(""); }
  2. var o = new C();
  3. document.write(o.p); // undefined

オブジェクトは、constructorプロパティを持ち、このプロパティはオブジェクトを生成した関数を指す。

JavaScript
  1. var F = function(param) { this.m = param; }
  2. var o = new F("hoge");
  3. document.write(o.m); // "hoge"
  4. document.write(o.constructor); // function (param) { this.m = param; }

クラスの差し替え

JavaScript はクラスベースではないので、オブジェクトのクラスを差し替えることができる。

JSON文字列をオブジェクト化したもののクラスを差し替えてみよう。次の例は, Object.setPrototypeOf() を使い, Foo クラスオブジェクトに変更している。

JavaScript
[RAW]
  1. var obj = JSON.parse('{"a":3, "b":4}');
  2. document.write(obj.constructor.name); // Object
  3. function Foo() { }
  4. Foo.prototype = {
  5. constructor: Foo,
  6. m1: function() { return this.a * this.b; }
  7. }
  8. // Object.setPrototypeOf() は IE11でサポートされない。
  9. if (Object.setPrototypeOf)
  10. Object.setPrototypeOf(obj, Foo.prototype); // 第2引数は prototype
  11. else
  12. obj.__proto__ = Foo.prototype;
  13. document.write( obj.constructor.name ); // Foo
  14. document.write( obj.m1() ); //=> 12
  15. document.write( obj.toString() ); // [object Object]

Object.setPrototypeOf() は ES2015 (ES6) で導入された。[[Prototype]] 内部プロパティを変更する。

# メソッド定義

JavaScriptのメソッドはただの関数で、オブジェクトのプロパティ値として参照されるもの。呼び出しのときのレシーバが this に格納される.

JavaScript
  1. function F(v) {
  2. this.val = v;
  3. this.twoTimes = function() { this.val *= 2; };
  4. }
  5. var o = new F(10);
  6. o.twoTimes();
  7. document.write(o.val); // 20

メソッドはただの関数なので, レシーバなしでも呼び出せる。クラス間で使いまわしもできる。

JavaScript
[RAW]
  1. function F(v) {
  2. this.val = v;
  3. }
  4. F.prototype.m1 = function(x) {
  5. console.log(this);
  6. return this.val * x;
  7. }
  8. var obj = new F(10);
  9. document.write(obj.m1(5)); //=> 50
  10. var f1 = F.prototype.m1;
  11. document.write(f1(3)); // TypeError: Cannot read property 'val' of undefined

レシーバなしに呼び出したとき, strictモードでは this は undefined になる。non-strictモードでは window になる.

this のスコープ

関数がクラスに従属しないのが JavaScript の特徴だが、this が状況によって想定しないオブジェクトを指すのは、混乱のもとになることが多い。

this は、他の変数と異なり、dynamic scope を持つ。字面上の外側でなく, 呼出し元の値を引き継ぐ。

次の例の data.map() に与えている関数内の this は, lexical に外側の this にはならず, map() がレシーバなしで呼び出すために undefined になる。

JavaScript
[RAW]
  1. const data = [1,2,3,4]
  2. function F() {
  3. this.val = 20;
  4. const r = data.map( function(value, index, array) {
  5. return value * (this ? this.val : 0);
  6. });
  7. console.log(r); //=> [0,0,0,0]
  8. }
  9. const obj = new F();

bind()

this を強制的に固定するために, bind関数がある。呼び出しのつどレシーバを差し替えるのは call 関数.

次の例は, メソッド (実体はたたの関数) の this を固定するために, コンストラクタ内で bind() している。コンストラクタ内では, this は必ずクラスのオブジェクトになるので。

JavaScript
[RAW]
  1. class TodoEdit extends React.Component
  2. {
  3. // props: onAdd
  4. constructor(props) {
  5. super(props);
  6. // 現に表示している内容
  7. this.state = { todoText: '' };
  8. // this を固定する
  9. this.handleSubmit = this.handleSubmit.bind(this);
  10. }
  11. handleSubmit(event) {
  12. // サーバに送信しない。後の例で, 送信するようにする.
  13. event.preventDefault();
  14. const text = this.state.todoText.trim();
  15. this.props.onAdd(text);
  16. this.setState( {todoText: ''} );
  17. }

アロー関数

ES2015 (ES6) で導入された「アロー関数」(Arrow function) は、基本的には無名関数の短い書き方だが、this を lexically に束縛 (bind) する点がことなる。

JavaScript
[RAW]
  1. class TodoEdit extends React.Component
  2. {
  3. // props: onAdd
  4. constructor(props) {
  5. super(props);
  6. // 現に表示している内容
  7. this.state = { todoText: '' };
  8. // 現代は, bind() よりも、アロー関数を使う.
  9. //this.handleSubmit = this.handleSubmit.bind(this);
  10. }
  11. handleSubmit = (event) => {
  12. // サーバに送信しない。後の例で, 送信するようにする.
  13. event.preventDefault();
  14. const text = this.state.todoText.trim();
  15. this.props.onAdd(text);
  16. this.setState( {todoText: ''} );
  17. }

アロー関数は、定義した時点の this を bind するので、上の例は呼び出し方に関わらず、this はクラスのオブジェクトを指すようになる。