1. ホーム
  2. javascript

[解決済み] プロトタイプ継承 - 執筆中 [重複].

2022-06-09 03:47:26

質問

というわけで、javascript.infoにある2つの例を紹介します。

例1:

var animal = {
  eat: function() {
    alert( "I'm full" )
    this.full = true
  }
}

var rabbit = {
  jump: function() { /* something */ }
}

rabbit.__proto__ = animal 

rabbit.eat() 

例2:

function Hamster() {  }
Hamster.prototype = {
  food: [],
  found: function(something) {
    this.food.push(something)
  }
}

// Create two speedy and lazy hamsters, then feed the first one
speedy = new Hamster()
lazy = new Hamster()

speedy.found("apple")
speedy.found("orange")

alert(speedy.food.length) // 2
alert(lazy.food.length) // 2 (!??)

例 2 から始める: コードが speedy.found に到達したとき found プロパティは speedy というプロパティがあり、プロトタイプまで登ってきてそこで変更します。そのため food.length はどちらのハムスターも同じ、つまり同じ胃袋を持っているのです。



このことから、存在しない新しいプロパティを追加して書き上げる場合、インタプリタはそのプロパティを見つけるまでプロトタイプの鎖を上がっていき、それから変更するのだと理解しています。

しかし、例1では別のことが起こります。

私たちは rabbit.eat を実行すると rabbit.full . full プロパティはどこにもないので、プロトタイプの連鎖を上がって(オブジェクトに?この例では、プロパティ fullrabbit が作成され、変更されます。一方、最初の例では、プロパティが見つからないため、プロトタイプチェーンに上がっていきます。

私は混乱しており、なぜこのようなことが起こるのかがわかりません。

どのように解決するのですか?

コンストラクタ関数の紹介

オブジェクトを作成するためのコンストラクタとして関数を使用することができます。コンストラクタ関数がPersonという名前の場合、そのコンストラクタで作成されるオブジェクトはPersonのインスタンスになります。

var Person = function(name){
  this.name = name;
};
Person.prototype.walk=function(){
  this.step().step().step();
};
var bob = new Person("Bob");

Personはコンストラクタ関数です。Personを使ってインスタンスを作成するときはnewキーワードを使わなければなりません。

var bob = new Person("Bob");console.log(bob.name);//=Bob
var ben = new Person("Ben");console.log(ben.name);//=Ben

プロパティ/メンバ name はインスタンス固有なので、bob と ben では異なります。

メンバーである walk はPerson.prototypeの一部で、すべてのインスタンスで共有されています。 bobとbenはPersonのインスタンスなので、walkメンバを共有しています(bob.walk==ben.walk)。

bob.walk();ben.walk();

walk()がbobに直接見つからなかったので、JavaScriptはPerson.prototypeでそれを探します。これはbobのコンストラクタだからです。もしそこで見つからなければ、Object.prototypeを探します。これをプロトタイプチェーンと呼びます。例えば、bob => Employee.prototype => Person.prototype => Object.prototype のように、プロトタイプ継承はこの鎖を長くすることで行われます(継承については後述します)。

bob、ben、その他すべてのPersonインスタンスがwalkを共有していても、walk関数内では this . の値は this の値は呼び出したオブジェクトになります。今は現在のインスタンスとしましょう。 bob.walk() はbobになります。("this"と呼び出しオブジェクトについては後で詳しく説明します)。

ベンが赤信号を待っていて、ボブが青信号のときに walk() を実行すると、ベンとボブでは明らかに異なることが起こります。

シャドーイングメンバーは、次のような場合に発生します。 ben.walk=22 のように、bob と ben が共有しているにもかかわらず walk 割り当て の22をben.walkに代入しても、bob.walkには影響しません。これは、このステートメントによって walk というメンバを作成し、それに22という値を割り当てるからです。ben.walkとPerson.prototype.walkという2種類のwalkメンバが存在することになります。

bob.walkを求めると、Person.prototype.walkの関数が得られます。 walk がbobに見つからなかったからです。しかし、ben.walkを求めると22という値が得られます。これはbenにwalkというメンバが作られ、JavaScriptがbenにwalkを見つけたため、Person.prototypeを探さないということです。

Object.create を 2 つの引数で使用する場合、Object.defineProperty または Object.defineProperties のシャドウイングは少し異なる動作をします。詳しくは はこちら .

プロトタイプの詳細

オブジェクトはプロトタイプを使用して他のオブジェクトを継承することができます。任意のオブジェクトのプロトタイプを他のオブジェクトに設定するには Object.create . コンストラクタ関数の紹介で、オブジェクトにメンバが見つからない場合、JavaScriptはプロトタイプチェーンでメンバを探すことを見てきました。

前のパートで、インスタンスのプロトタイプから来たメンバー(ben.walk)の再割り当てが、そのメンバーをシャドウする(Person.prototype.walkを変更するのではなく、benにwalkを作る)ことを見てきました。

再割り当てではなく、メンバーを変異させた場合はどうでしょうか?ミューテートとは、(例えば)オブジェクトのサブプロパティを変更したり、オブジェクトの値を変更する関数を呼び出したりすることです。例えば

var o = [];
var a = o;
a.push(11);//mutate a, this will change o
a[1]=22;//mutate a, this will change o

以下のコードでは、プロトタイプメンバとインスタンスメンバの違いをメンバの変異によって示しています。

var person = {
  name:"default",//immutable so can be used as default
  sayName:function(){
    console.log("Hello, I am "+this.name);
  },
  food:[]//not immutable, should be instance specific
         //  not suitable as prototype member
};
var ben = Object.create(person);
ben.name = "Ben";
var bob = Object.create(person);
console.log(bob.name);//=default, setting ben.name shadowed the member
                      //  so bob.name is actually person.name
ben.food.push("Hamburger");
console.log(bob.food);//=["Hamburger"], mutating a shared member on the
// prototype affects all instances as it changes person.food
console.log(person.food);//=["Hamburger"]

上のコードは、benとbobがpersonからメンバーを共有していることを示しています。personは1つだけで、bobとbenのプロトタイプとして設定されています(personはインスタンスに存在しない要求されたメンバーを探すためにプロトタイプチェーンの最初のオブジェクトとして使用されます)。上記のコードの問題点は、bob と ben はそれぞれ自分の food というメンバが必要なことです。ここでコンストラクタ関数の出番です。これはインスタンス固有のメンバを作成するために使用されます。また、この関数に引数を渡すことで、インスタンス固有のメンバの値を設定することができます。

次のコードはコンストラクタ関数を実装する別の方法を示しており、構文は異なりますが、考え方は同じです。

  1. 多くのインスタンスに共通するメンバーを持つオブジェクトを定義します (person は bob と ben の設計図であり、jilly、marie、clair にもなりえます...)。
  2. インスタンスに固有なメンバーを定義する(bobとben)。
  3. ステップ2のコードを実行するインスタンスを作成します。

コンストラクタ関数では、ステップ2でプロトタイプを設定しますが、次のコードではステップ3でプロトタイプを設定します。

このコードでは、インスタンスを作成するときにほとんどすぐにシャドウする可能性が高いので、foodと同様にprototypeからnameを削除しています。name はインスタンス固有のメンバになり、コンストラクタ関数でデフォルト値が設定されます。また、foodのメンバもprototypeからインスタンス固有のメンバに移動しているので、benにfoodを追加する際にbob.foodに影響を与えることはありません。

var person = {
  sayName:function(){
    console.log("Hello, I am "+this.name);
  },
  //need to run the constructor function when creating
  //  an instance to make sure the instance has
  //  instance specific members
  constructor:function(name){
    this.name = name || "default";
    this.food = [];
    return this;
  }
};
var ben = Object.create(person).constructor("Ben");
var bob = Object.create(person).constructor("Bob");
console.log(bob.name);//="Bob"
ben.food.push("Hamburger");
console.log(bob.food);//=[]

オブジェクトの作成とオブジェクトの定義を支援するためのより強固な類似のパターンに出会うかもしれません。

継承

次のコードは、継承の方法を示しています。タスクは基本的に以前のコードと同じですが、少し追加で

  1. オブジェクトのインスタンス固有のメンバを定義する(関数HamsterとRussionMini)。
  2. 継承のプロトタイプ部分を設定する(RussionMini.prototype = Object.create(Hamster.prototype) )。
  3. インスタンス間で共有できるメンバを定義する(Hamster.prototypeとRussionMini.prototype)
  4. ステップ1のコードを実行するインスタンスを作成し、継承するオブジェクトには親コードも実行させる(Hamster.apply(this,arguments);)

古典的な継承と呼ばれるパターンを使っています。もし構文で混乱するようであれば、もっと説明するか、別のパターンを提供したいと思います。

function Hamster(){
 this.food=[];
}
function RussionMini(){
  //Hamster.apply(this,arguments) executes every line of code
  //in the Hamster body where the value of "this" is
  //the to be created RussionMini (once for mini and once for betty)
  Hamster.apply(this,arguments);
}
//setting RussionMini's prototype
RussionMini.prototype=Object.create(Hamster.prototype);
//setting the built in member called constructor to point
// to the right function (previous line has it point to Hamster)
RussionMini.prototype.constructor=RussionMini;
mini=new RussionMini();
//this.food (instance specic to mini)
//  comes from running the Hamster code
//  with Hamster.apply(this,arguments);
mini.food.push("mini's food");
//adding behavior specific to Hamster that will still be
//  inherited by RussionMini because RussionMini.prototype's prototype
//  is Hamster.prototype
Hamster.prototype.runWheel=function(){console.log("I'm running")};
mini.runWheel();//=I'm running

Object.createでプロトタイプを設定し、継承する。

に関するドキュメントはこちらです。 Object.create に関するドキュメントです。それは基本的に、最初の引数を返されたオブジェクトのプロトタイプとして、2番目の引数(ポリフィルではサポートされていません)を返します。

もし第2引数が与えられなかった場合は、返されたオブジェクトのプロトタイプとして使用される第1引数(返されたオブジェクトのプロトタイプチェーンで使用される最初のオブジェクト)を持つ空のオブジェクトが返されるでしょう。

RussionMini のプロトタイプを Hamster のインスタンスに設定する (RussionMini.prototype = new Hamster()) 場合もあります。これは、同じことを達成する (RussionMini.prototype のプロトタイプは Hamster.prototype) にもかかわらず、Hamster インスタンス メンバを RussionMini.prototype のメンバとして設定するため、望ましくないと言えます。そのため、RussionMini.prototype.foodは存在しますが、共有メンバーです("More about prototype"のbobとbenを覚えていますか)。Hamsterのコードは、RussionMiniの作成時に Hamster.apply(this,arguments); で実行され、次に this.food = [] を実行しますが、Hamster のメンバーは RussionMini.prototype のメンバーであることに変わりはありません。

もう一つの理由は、ハムスターを作成するために、まだ利用できない可能性がある渡された引数に対して多くの複雑な計算を行う必要があることです。




親関数の拡張とオーバーライド 親関数の拡張とオーバーライド

時々 children を拡張する必要があります。 parent 関数を拡張する必要があります。

子」(=RussionMini)に何か余計なことをさせたいのですね。RussionMiniがHamsterのコードを呼び出して何かをさせ、さらに何かをさせることができれば、HamsterのコードをRussionMiniにコピー&ペーストする必要はありません。

次の例では、ハムスターは1時間に3km走ることができるが、RussionMiniはその半分の速さしか走れないと仮定しています。RussionMini では 3/2 をハードコードできますが、この値が変更された場合、コード内の複数の場所を変更する必要があります。ここでは、Hamster.prototypeを使用して親(Hamster)の速度を取得する方法を説明します。

var Hamster = function(name){
 if(name===undefined){
   throw new Error("Name cannot be undefined");
 }
 this.name=name;
}
Hamster.prototype.getSpeed=function(){
  return 3;
}
Hamster.prototype.run=function(){
  //Russionmini does not need to implement this function as
  //it will do exactly the same as it does for Hamster
  //But Russionmini does need to implement getSpeed as it
  //won't return the same as Hamster (see later in the code) 
  return "I am running at " + 
    this.getSpeed() + "km an hour.";
}

var RussionMini=function(name){
  Hamster.apply(this,arguments);
}
//call this before setting RussionMini prototypes
RussionMini.prototype = Object.create(Hamster.prototype);
RussionMini.prototype.constructor=RussionMini;

RussionMini.prototype.getSpeed=function(){
  return Hamster.prototype
    .getSpeed.call(this)/2;
}    

var betty=new RussionMini("Betty");
console.log(betty.run());//=I am running at 1.5km an hour.

デメリットは、Hamster.prototypeをハードコーディングしてしまうことです。の利点を生かせるパターンがあるかもしれません。 super をJavaのように使うことができます。

私が見たパターンのほとんどは、継承レベルが2レベル以上(Child => Parent => GrandParent)になると壊れるか、superを経由して実装することでより多くのリソースを使用することになります。 クロージャ .

親メソッド(=Hamster)をオーバーライドする場合も同様ですが、Hamster.prototype.parentMethod.call(this,...)は行いません。

this.constructor

コンストラクタのプロパティはJavaScriptによってプロトタイプに含まれているので、変更することができますが、コンストラクタ関数を指すようにしなければなりません。そのため Hamster.prototype.constructor はハムスターを指しているはずです。

もし、プロトタイプを設定した後、継承の一部が正しい関数を指すようにする必要があります。

var Hamster = function(){};
var RussionMinni=function(){
   // re use Parent constructor (I know there is none there)
   Hamster.apply(this,arguments);
};
RussionMinni.prototype=Object.create(Hamster.prototype);
console.log(RussionMinni.prototype.constructor===Hamster);//=true
RussionMinni.prototype.haveBaby=function(){
  return new this.constructor();
};
var betty=new RussionMinni();
var littleBetty=betty.haveBaby();
console.log(littleBetty instanceof RussionMinni);//false
console.log(littleBetty instanceof Hamster);//true
//fix the constructor
RussionMinni.prototype.constructor=RussionMinni;
//now make a baby again
var littleBetty=betty.haveBaby();
console.log(littleBetty instanceof RussionMinni);//true
console.log(littleBetty instanceof Hamster);//true

多重継承とミックスイン

継承しない方が良いものもあります。もし、Catが移動できるのであれば、CatはMovableから継承しない方が良いでしょう。Cat は Movable ではなく、Cat は動くことができます。クラスベースの言語では、CatはMovableを実装しなければならないでしょう。JavaScriptでは、Movableを定義し、ここで実装を定義することができ、Catはそれをオーバーライド、拡張、またはデフォルトの実装を使用することができます。

Movableでは、インスタンス固有のメンバ(例えば location ). また、インスタンスに依存しないメンバ(関数move()など)もあります。インスタンス固有のメンバーは、インスタンス生成時にmxIns(mixinヘルパー関数で追加)を呼び出すことで設定されます。プロトタイプのメンバーは、Movable.prototypeからmixinヘルパー関数を使ってCat.prototypeに1つずつコピーされます。

var Mixin = function Mixin(args){
  if(this.mixIns){
    i=-1;len=this.mixIns.length;
    while(++i<len){
        this.mixIns[i].call(this,args);
      }
  }  
};
Mixin.mix = function(constructor, mix){
  var thing
  ,cProto=constructor.prototype
  ,mProto=mix.prototype;
  //no extending, if multiple prototypes
  // have members with the same name then use
  // the last
  for(thing in mProto){
    if(Object.hasOwnProperty.call(mProto, thing)){
      cProto[thing]=mProto[thing];
    }
  }
  //instance intialisers
  cProto.mixIns = cProto.mixIns || [];
  cProto.mixIns.push(mix);
};
var Movable = function(args){
  args=args || {};
  //demo how to set defaults with truthy
  // not checking validaty
  this.location=args.location;
  this.isStuck = (args.isStuck===true);//defaults to false
  this.canMove = (args.canMove!==false);//defaults to true
  //speed defaults to 4
  this.speed = (args.speed===0)?0:(args.speed || 4);
};
Movable.prototype.move=function(){
  console.log('I am moving, default implementation.');
};
var Animal = function(args){
  args = args || {};
  this.name = args.name || "thing";
};
var Cat = function(args){
  var i,len;
  Animal.call(args);
  //if an object can have others mixed in
  //  then this is needed to initialise 
  //  instance members
  Mixin.call(this,args);
};
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;
Mixin.mix(Cat,Movable);
var poochie = new Cat({
  name:"poochie",
  location: {x:0,y:22}
});
poochie.move();

上記は、同じ名前の関数を、最後に混ざったものに置き換えるという単純な実装です。

この変数が

すべてのサンプルコードで、あなたは this は現在のインスタンスを参照しています。

this変数は実際には呼び出したオブジェクトを参照しており、関数の前に来たオブジェクトを参照しています。

明確にするために、次のコードを見てください。

theInvokingObject.thefunction();

これが間違ったオブジェクトを参照する例は、通常、イベントリスナー、コールバック、タイムアウト、インターバルをアタッチするときです。次の2行のコードでは pass で関数を渡していますが、これは呼び出しません。関数を渡すのは someObject.aFunction であり、それを呼び出すことは someObject.aFunction() . は this の値は関数が宣言されているオブジェクトではなく invokes である。

setTimeout(someObject.aFuncton,100);//this in aFunction is window
somebutton.onclick = someObject.aFunction;//this in aFunction is somebutton

を作るには this が someObject を参照するようにするために クロージャを渡すことができます。 を渡すことができます。

setTimeout(function(){someObject.aFuncton();},100);
somebutton.onclick = function(){someObject.aFunction();};

を返す関数を定義するのが好きです。 クロージャ に含まれる変数を細かく制御するために、プロトタイプ上で クロージャ のスコープに含まれる変数を細かく制御することができます。

var Hamster = function(name){
  var largeVariable = new Array(100000).join("Hello World");
  // if I do 
  // setInterval(function(){this.checkSleep();},100);
  // then largeVariable will be in the closure scope as well
  this.name=name
  setInterval(this.closures.checkSleep(this),1000);
};
Hamster.prototype.closures={
  checkSleep:function(hamsterInstance){
    return function(){
      console.log(typeof largeVariable);//undefined
      console.log(hamsterInstance);//instance of Hamster named Betty
      hamsterInstance.checkSleep();
    };
  }
};
Hamster.prototype.checkSleep=function(){
  //do stuff assuming this is the Hamster instance
};

var betty = new Hamster("Betty");

(コンストラクタ)引数の受け渡し

Child が Parent を呼び出すとき ( Hamster.apply(this,arguments); ) を呼び出すとき、Hamster は RussionMini と同じ引数を同じ順序で使用すると仮定します。他の関数を呼び出す関数については、私は通常、引数を渡すために別の方法を使用します。

私は通常、関数に1つのオブジェクトを渡し、その関数に必要なものを変異させます(デフォルトを設定します)、そしてその関数は別の関数にそれを渡し、同じことを行います。以下はその例です。

//helper funciton to throw error
function thowError(message){
  throw new Error(message)
};
var Hamster = function(args){
  //make sure args is something so you get the errors
  //  that make sense to you instead of "args is undefined"
  args = args || {};
  //default value for type:
  this.type = args.type || "default type";
  //name is not optional, very simple truthy check f
  this.name = args.name || thowError("args.name is not optional");
};
var RussionMini = function(args){
  //make sure args is something so you get the errors
  //  that make sense to you instead of "args is undefined"
  args = args || {};
  args.type = "Russion Mini";
  Hamster.call(this,args);
};
var ben = new RussionMini({name:"Ben"});
console.log(ben);// Object { type="Russion Mini", name="Ben"}
var betty = new RussionMini();//Error: args.name is not optional

関数チェーンで引数を渡すこの方法は、多くの場合において有用です。何かの合計を計算するようなコードを書いていて、後でその合計をある通貨に直す場合、通貨の値を渡すために多くの関数を変更する必要があるかもしれません。通貨の値をアップスコープで指定することもできます (グローバルに指定することもできます。 window.currency='USD' のようにグローバルにすることもできます)しかし、それは解決するための悪い方法です。

オブジェクトを渡すことで、通貨を args に追加し、他の関数を変更することなく、必要なときにそれを変更/使用することができます(明示的に関数呼び出しの中でそれを渡す必要があります)。

プライベート変数

JavaScriptにはprivate修飾子がありません。

以下に同意します。 http://blog.millermedeiros.com/a-case-against-private-variables-and-functions-in-javascript/ であり、個人的には使用していません。

他のプログラマにメンバがプライベートであることを示すには、その名前を _aPrivate というオブジェクト変数にすべてのプライベート変数を入れるか、あるいは _ .

プライベートメンバを実装するには クロージャ で実装できますが、インスタンス固有のプライベートメンバは、プロトタイプにない関数からしかアクセスすることができません。

privateをクロージャとして実装しないことは、実装を漏らし、あなたやあなたのコードを拡張するユーザが、あなたの公開APIの一部ではないメンバーを使用することを可能にします。これは良くも悪くもありえます。

良い点は、あなたや他の人がテストのために特定のメンバーを簡単にモックできるようになることです。しかし、次のバージョンのコードが同じ実装やプライベートなメンバーを持っているという保証はないため、これは悪いことでもあります。

クロージャを使うことで、他の人に選択肢を与えず、ドキュメントで命名規則を使うことで、選択肢を与えることになります。これはJavaScriptに限ったことではなく、他の言語でも、他の人が何をしているかを知っていることを信頼し、彼らが望むように(リスクを伴って)選択できるように、プライベートメンバーを使用しないことを決定することができます。

それでもプライベートメンバーにこだわるのであれば に続く パターンが役に立つかもしれません。しかし、これはprivateを実装しているのではなく、protectedを実装しています。