抹桥的博客
Language
Home
Archive
About
GitHub
Language
主题色
250
2073 文字
10 分
JavaScript におけるクラスと継承

JavaScriptがプロトタイプベースの言語であることは、周知の事実です。オブジェクト自身が持たないプロパティを呼び出すと、JavaScriptはそのオブジェクトのプロトタイプオブジェクトからそのプロパティを探し、プロトタイプにもそのプロパティがなければ、さらにそのプロトタイプのプロトタイプを探し、最終的にプロトタイプチェーンの末端、つまりObject.prototypeのプロトタイプであるnullに到達するまで探し続けます。このプロパティ探索の仕組みを「プロトタイプチェーン」と呼びます。

クラスの実装#

JavaScript自体にはクラスの概念がありません。そのため、クラスを実装する場合、通常はコンストラクタ関数を使ってクラスの振る舞いを模倣します。

function Person(name, age) {
  //实现一个类
  this.name = name;
  this.age = age;
}
var you = new Person("you", 23); //通过 new 来新建实例

まず、Personというコンストラクタ関数を新しく作成します。一般的な関数と区別するため、コンストラクタ関数にはCamelCase方式で名前を付けます。 次に、new演算子を使ってインスタンスを作成します。new演算子は実際には以下のいくつかのことを行っています。

  1. Person.prototypeを継承した新しいオブジェクトを作成します。
  2. コンストラクタ関数Personが実行される際、対応する引数が渡され、同時にコンテキストがこの新しく作成されたオブジェクトに指定されます。
  3. コンストラクタ関数がオブジェクトを返した場合、そのオブジェクトがnewの結果に置き換わります。コンストラクタ関数がオブジェクト以外を返した場合、その戻り値は無視されます。
返回值不是对象;
function Person(name) {
  this.name = name;
  return "person";
}
var you = new Person("you");
//  you 的值: Person {name: "you"}
返回值是对象;
function Person(name) {
  this.name = name;
  return [1, 2, 3];
}
var you = new Person("you");
//  you的值: [1,2,3]

クラスのインスタンスがクラスのメソッドを共有する必要がある場合、コンストラクタ関数のprototypeプロパティにメソッドを追加する必要があります。new演算子で作成されたオブジェクトはすべてコンストラクタ関数のprototypeプロパティを継承しているためです。それらのインスタンスは、クラスのprototypeに定義されたメソッドやプロパティを共有できます。

function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype = {
  sayName: function () {
    console.log("My name is", this.name);
  },
};
var you = new Person("you", 23);
var me = new Person("me", 23);
you.sayName(); // My name is you.
me.sayName(); // My name is me.

継承の実装#

JavaScriptで一般的に使われる継承方法は、コンビネーション継承です。これは、コンストラクタ関数とプロトタイプチェーン継承を同時に用いて、継承の実現を模倣するものです。

//Person 构造函数如上
function Student(name, age, clas) {
  Person.call(this, name, age);
  this.clas = clas;
}
Student.prototype = Object.create(Person.prototype); // Mark 1
Student.prototype.constructor = Student; //如果不指明,则 Student 的 constructor 是 Person
Student.prototype.study = function () {
  console.log("I study in class", this.clas);
};
var liming = new Student("liming", 23, 7);
liming instanceof Person; //true
liming instanceof Student; //true
liming.sayName(); // My name is liming
liming.study(); // I study in class 7

コード中のMark 1では、Object.createメソッドが使用されています。これはES5で追加されたメソッドで、指定されたプロトタイプを持つオブジェクトを作成するために使われます。環境が互換性がない場合、以下のPolyfillで実装できます(最初の引数のみ実装)。

if (!Object.create) {
  Object.create = function (obj) {
    function F() {}
    F.prototype = obj;
    return new F();
  };
}

これは実際には、objを一時的な関数Fに代入し、Fのインスタンスを返すというものです。こうすることで、コードのMark 1にあるように、StudentPerson.prototype上のすべてのプロパティを取得します。では、なぜPerson.prototypeを直接Student.prototypeに代入しないのか、と疑問に思う人もいるかもしれません。

はい、直接代入することで、子クラスが親クラスのprototypeを共有するという目的は達成できます。しかし、それはプロトタイプチェーンを破壊してしまいます。つまり、子クラスと親クラスが同じprototypeを共有することになり、ある子クラスがprototypeを変更すると、同時に親クラスのprototypeも変更されてしまいます。その結果、この親クラスに基づいて作成されたすべてのサブクラスに影響を与えてしまい、これは私たちが望む結果ではありません。例を見てみましょう。

//Person 同上
//Student 同上
Student.prototype = Person.prototype;
Student.prototype.sayName = function () {
  console.log("My name is", this.name, "my class is", this.clas);
};
var liming = new Student("liming", 23, 7);
liming.sayName(); //My name is liming,my class is 7;
//另一个子类
function Employee(name, age, salary) {
  Person.call(name, age);
  this.salary = salary;
}
Employee.prototype = Person.prototype;
var emp = new Employee("emp", 23, 10000);
emp.sayName(); //Mark 2

Mark 2は何を出力すると思いますか?

私たちが期待するMark 2は、「My name is emp」と出力されるはずです。しかし実際にはエラーが発生します。なぜでしょうか?Student.prototypeを書き換えた際に、同時にPerson.prototypeも変更してしまったためです。その結果、empが継承するprototypeは私たちが意図しないものとなり、そのsayNameメソッドはMy name is',this.name,'my class is',this.clasとなってしまい、当然エラーが発生します。

ES6の継承#

ECMAScript 6のリリースに伴い、継承を実現する新しい方法が生まれました。それがclassキーワードによるものです。

クラスの実装#

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  sayHello() {
    console.log(`My name is ${this.name},i'm ${this.age} years old`);
  }
}
var you = new Person("you", 23);
you.sayHello(); //My name is you,i'm 23 years old.

継承#

ES6における継承も非常に便利で、extendsキーワードを使って実現します。

class Student extends Person {
  constructor(name, age, cla) {
    super(name, age);
    this.class = cla;
  }
  study() {
    console.log(`I'm study in class ${this.class}`);
  }
}
var liming = new Student("liming", 23, 7);
liming.study(); // I'm study in class 7.

この継承は、上記のES5で実装された継承に比べてはるかに便利になりました。しかし、実際には原理は同じで、提供されているこれらのキーワードは単なるシンタックスシュガーに過ぎず、JavaScriptがプロトタイプベースであるという事実は変わっていません。ただし、extendsで実現される継承には一つ制限があり、プロパティを定義できず、メソッドのみ定義可能です。新しいプロパティを追加するには、やはりprototypeを変更して目的を達成する必要があります。

Student.prototype.teacher = "Mr.Li";
var liming = new Student("liming", 23, 7);
var hanmeimei = new Student("hanmeimei", 23, 7);
liming.teacher; //Mr.Li
hanmeimei.teacher; //Mr.Li

静的メソッド#

ES6では、staticキーワードも提供されており、静的メソッドを実現できます。静的メソッドは継承可能ですが、クラス自体からのみ呼び出すことができ、インスタンスからは呼び出せません。

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  static say() {
    console.log("Static");
  }
}
class Student extends Person {}
Person.say(); // Static
Student.say(); // Static
var you = new Person("you", 23);
you.say(); // TypeError: liming.say is not a function

インスタンスから呼び出すと、直接エラーが発生することがわかります。

Superキーワード#

子クラスでは、superを使って親クラスを呼び出すことができます。呼び出し位置によって、その振る舞いは異なります。constructor内で呼び出す場合、親クラスのconstructorメソッドを呼び出すことに相当し、通常のメソッド内で呼び出す場合は、親クラス自体を呼び出すことに相当します。

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  sayHello() {
    console.log(`My name is ${this.name},i'm ${this.age} years old`);
  }
}
class Student extends Person {
  constructor(name, age, cla) {
    super(name, age); // 必须在子类调用 this 前执行,调用了父类的 constructor
    this.class = cla;
  }
  sayHello() {
    super.sayHello; // 调用父类方法
    console.log("Student say");
  }
}
var liming = new Student("liming", 23, 7);
liming.say(); // My name is liming,i'm 23 years old.\n Student say.

まとめ#

ここまでで、ES6のリリース以降、JavaScriptで継承を実現する標準的な方法ができたことがわかります。これらは単なるシンタックスシュガーであり、その背後にある本質はプロトタイプチェーンとコンストラクタ関数によって実現されていますが、記述方法がより理解しやすく、かつ明確になりました。

参考:

この記事は 2016年3月22日 に公開され、2016年3月22日 に最終更新されました。3485 日が経過しており、内容が古くなっている可能性があります。

JavaScript におけるクラスと継承
https://blog.kisnows.com/ja-JP/2016/03/22/class-and-inherit-in-javascript/
作者
Kisnows
公開日
2016-03-22
ライセンス
CC BY-NC-ND 4.0