メインコンテンツへ移動

JS 学習ノート 2_オブジェクト指向

無料2015-04-06#JS#js面向对象

JS の OOP は Java などと大きく異なり、その核心は prototype です。本稿では JS の継承と OOP のその他の部分について詳細に分析します

##1.オブジェクトの定義##

ECMAScript において、オブジェクトとは順序のないプロパティの集合であり、ここでの「プロパティ」とは基本値、オブジェクト、または関数であり得ます

##2.データプロパティとアクセサプロパティ##

  • データプロパティとは値を持つプロパティのことで、プロパティを読み取り専用、削除不可、列挙不可などに設定できます

  • アクセサプロパティは getter と setter を設定するために使用され、プロパティ名の前に"_"(アンダースコア)を付けることで、そのプロパティはアクセサを通じてのみアクセス可能(プライベートプロパティ)であることを示しますが、アンダースコアを付けたからといってプロパティがプライベートになるわけではありません。これは単に習慣的な命名規則の一つに過ぎません。アクセサプロパティはあまり役に立ちません。理由は以下の通りです:

     var book={
       _year:2004,
       edition:1
     }
     Object.defineProperty(book,"year",{
            get:function(){
               return this._year;
            },
            set:function(newValue){
               if(newValue>2004){
                 this._year=newValue;
                 this.edition+=newValue-2004;
               }
            }
     });
     book.year=2005;
     alert(book.edition);
     /*
     for(var attr in book){
       showLine(attr + ' = ' + book[attr]);
     }
     */
     
    

高程では上記のサンプルコードを使用しており、原理としては book オブジェクトのプロパティのうち_year はデータプロパティ、year はアクセサプロパティであり、getter と setter を利用して読み書き制御を挿入できるというもので、听起来は良いです。

しかし問題は_year と edition はどちらも列挙可能であり、つまり for...in ループで見ることができますが、アクセサプロパティ year は列挙不可です。公開すべきアクセサは公開されず、隠すべきプライベートプロパティが公開されてしまいます。

それに加え、このようにアクセサを定義する方式は全ブラウザ互換ではなく、[IE9+] で完全にサポートされています。もちろん、旧ブラウザ向けの方法(__defineGetter__() と__defineSetter__())もありますが、かなり面倒です。

总之、アクセサプロパティはあまり役に立ちません

##3.コンストラクタ##

function Fun(){} var fun = new Fun();ここで Fun はコンストラクタであり、通常関数と宣言方法の違いは全くなく、呼び出し方が異なる(new 演算子で呼び出す)だけです

コンストラクタはカスタムタイプを定義するために使用できます。例えば:

function MyClass(){
  this.attr = value;//メンバー変数
  this.fun = function(){...}//メンバー関数
}

Java のクラス宣言とやや似ていますが、いくつかの違いもあります。例えば this.fun は単なる関数ポインタであるため、完全に他のアクセス可能な関数(グローバル関数など)を指すようにできますが、这样做するとカスタムオブジェクトのカプセル性を破壊します

##4.関数とプロトタイプ prototype##

  1. 関数を宣言すると同時にプロトタイプオブジェクトが作成され、関数名がそのプロトタイプオブジェクトへの参照を保持します(fun.prototype)

  2. プロトタイプオブジェクトにプロパティを追加することも、インスタンスオブジェクトにプロパティを追加することもできます。違いは、プロトタイプオブジェクトのプロパティはそのタイプのすべてのインスタンスで共有されるのに対し、インスタンスオブジェクトのプロパティは非共有であることです

  3. インスタンスオブジェクトのプロパティにアクセスする際、まずそのインスタンスオブジェクトのスコープ内で検索し、見つからない場合にのみプロトタイプオブジェクトのスコープ内で検索するため、インスタンスのプロパティはプロトタイプオブジェクトの同名プロパティをシャドウできます

  4. プロトタイプオブジェクトの constructor プロパティは関数ポインタであり、関数宣言を指します

  5. プロトタイプを通じてネイティブ参照タイプ(Object、Array、String など)にカスタムメソッドを追加できます。例えば String に Chrome ではサポートされていないが FF ではサポートされている startsWith メソッドを追加:

    var str = 'this is script';
    //alert(str.startsWith('this'));//Chrome でエラー
    String.prototype.startsWith = function(strTarget){
      return this.indexOf(strTarget) === 0;
    }
    alert(str.startsWith('this'));//エラーにならない
    

注意:ネイティブオブジェクトにプロトタイププロパティを追加することは推奨されません。これによりネイティブメソッドを意図せず上書きし、他のネイティブコード(そのメソッドを呼び出しているネイティブコード)に影響を与える可能性があるためです

  1. プロトタイプを通じて継承を実現できます。考え方はサブクラスの prototype プロパティを親クラスのインスタンスを指すようにし、サブクラスがアクセス可能なプロパティを増やすことです。したがってプロトタイプチェーンで接続した後

    サブクラスがアクセス可能なプロパティ
    = サブクラスインスタンスプロパティ + サブクラスプロトタイププロパティ
    = サブクラスインスタンスプロパティ + 親クラスインスタンスプロパティ + 親クラスプロトタイププロパティ
    = サブクラスインスタンスプロパティ + 親クラスインスタンスプロパティ + 親親クラスインスタン��プロパティ + 親親クラスプロトタイププロパティ
    = ...
    

最終的に親親...親クラスプロトタイププロパティは Object.prototype が指すプロトタイプオブジェクトのプロパティに置き換えられます

具体的な実装は SubType.prototype = new SuperType();で、これをシンプルプロトタイプチェーン方式の継承と呼べます

##5.カスタムタイプを作成する最良の方法(コンストラクタパターン + プロトタイプパターン)##

インスタンスプロパティはコンストラクタで宣言し、共有プロパティはプロトタイプで宣言します。具体的な実装は以下の通り:

    function MyObject(){
      //インスタンスプロパティ
      this.attr = value;
      this.arr = [value1, value2...];
    }
    MyObject.prototype = {
      constructor: MyObject,//サブクラスが正しいコンストラクタ参照を保持することを保証。否则サブクラスインスタンスの constructor は Object のコンストラクタを指すことになります。プロトタイプを匿名オブジェクトに変更したため
      //共有プロパティ
      fun: function(){...}
    }
    

##6.継承を実現する最良の方法(パラサイトコンボネーション継承)##

function object(obj){//プロトタイプが obj でインスタンスプロパティがないオブジェクトを返す
  function Fun(){}
  Fun.prototype = obj;
  return new Fun();
}
function inheritPrototype(subType, superType){
  var prototype = object(superType.prototype);//プロトタイプチェーンを構築し、親クラスプロトタイププロパティを継承。カスタム関数 object で処理するのは、サブクラスプロトタイプとなる親クラスインスタンスがインスタンスプロパティを持たないようにするため。簡単に言えば、余分なインスタンスプロパティを切り落とすため。コンボネーション継承と比較して理解できます
  prototype.constructor = subType;//コンストラクタが正しいことを保証。プロトタイプチェーンはサブクラスが保持するコンストラクタ参照を変更するため、プロトタイプチェーン構築後に元に戻す必要がある
  subType.prototype = prototype;
}

function SubType(arg1, arg2...){
  SuperType.call(this, arg1, arg2...);//親クラスインスタンスプロパティを継承
  this.attr = value;//サブクラスインスタンスプロパティ
}
inheritPrototype(SubType, SuperType);

具体的な説明はコードコメントを参照。この方式は SubType.prototype = new SuperType();シンプルプロトタイプチェーンの欠点を回避します:

  • サブクラスインスタンスが親クラスインスタンス参照プロパティを共有する問題(プロトタイプ参照プロパティはすべてのインスタンスで共有されるため、プロトタイプチェーンを構築すると親クラスのインスタンスプロパティが自然にサブクラスのプロトタイププロパティになる)。

  • サブクラスインスタンス作成時に親クラスコンストラクタにパラメータを渡せない

##7.継承を実現する最も一般的な方式(コンボネーション継承)##

上記の inheritPrototype(SubType, SuperType);文を以下に置き換え:

SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;

この方式の欠点は:親クラスのコンストラクトメソッドを 2 回呼び出すため、余分な親クラスインスタンスプロパティが存在することです。具体的な理由は以下の通り:

1 回目の SuperType.call(this);文で親クラスから親クラスインスタンスプロパティを 1 コピーしてサブクラスのインスタンスプロパティとし、2 回目の SubType.prototype = new SuperType();で親クラスインスタンスを作成してサブクラスプロトタイプとします。此时この親クラスインスタンスにはまた 1 つのインスタンスプロパティがありますが、これは 1 回目にコピーされたインスタンスプロパティでシャドウされてしまうため、余分です。

コメント

コメントはまだありません

コメントを書く