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
演算子は実際には以下のいくつかのことを行っています。
Person.prototype
を継承した新しいオブジェクトを作成します。- コンストラクタ関数
Person
が実行される際、対応する引数が渡され、同時にコンテキストがこの新しく作成されたオブジェクトに指定されます。 - コンストラクタ関数がオブジェクトを返した場合、そのオブジェクトが
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にあるように、Student
はPerson.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 日が経過しており、内容が古くなっている可能性があります。