オブジェクト指向は、問題を構成するトランザクションを各オブジェクトに分解し、オブジェクトを作成する目的は、ステップを完了するためではなく、全体の問題を解決する過程で発生する事象を記述することにあります。これは、汎用コードを書くこと、コードの再利用を強化し、差異を隠すことを意図しています。
一、オブジェクト指向プログラミングとは#
js はプロトタイプベース
であり、オブジェクト指向プログラミング
に基づいています。
オブジェクト指向とは、データとそのデータに対する操作方法を一緒にまとめて、全体として扱うこと、すなわちオブジェクトとして扱うことです。同種のオブジェクトから共通点を抽象化し、クラスを形成します。
1. 手続き型プログラミング#
プロジェクト(またはイベント)を最初から最後まで順番に、一歩ずつ完了させる方法で、何を先に行い、何を後に行うかを決めて、終了まで進めるのは、私たち人間が物事を行う方法でもあります。
トップダウン方式で、全体のフレームワークを決定し、その後、少しずつ実現したい効果を追加していく方法で、シンプルなシステムに適しており、理解しやすいですが、複雑なシステムには対応しづらく、メンテナンスや拡張が難しく、再利用が困難です。
手続き型は、問題を分析して解決する手順を考え、それを関数で一歩ずつ実現し、使用時にそれを呼び出すことを強調します。これは、私たちの日常的な思考に近いです。
2. オブジェクト指向プログラミング#
プロジェクト(またはイベント)をより小さなプロジェクトに分割し、各部分が特定の機能を担当し、最終的にこれらの部分が全体を構成する方法です。まずコンポーネントを設計し、次に組み立てを完成させる方法で、大規模で複雑なシステムに適しています。
オブジェクト指向は、問題を構成するトランザクションを各オブジェクトに分解し、オブジェクトを作成する目的は、ステップを完了するためではなく、全体の問題を解決する過程で発生する事象を記述することにあります。これは、汎用コードを書くこと、コードの再利用を強化し、差異を隠すことを意図しています。
オブジェクト指向を理解するには、まずクラスとオブジェクトの概念を理解する必要があります。
二、オブジェクトを作成する方法#
1. リテラルとインスタンスの作成#
window.onload = function() {
// インスタンス
var person = new Object();
person.name = '小明';
person.age = 22;
person.year = function() {
console.log(this.name + 'は今年' + this.age + '歳です!')
};
person.year();
// リテラル
var student = {
name: '小明',
age: 22,
year: function () {
console.log(this.name + 'は今年' + this.age + '歳です!')
}
}
student.year();
}
// 小明は今年22歳です!
両者の出力結果は同じで、コンソールに出力されます:
欠点:オブジェクトの重複インスタンス化、コードの冗長性が高い
2. ファクトリーパターン#
window.onload = function() {
function createObj(name, age) {
var obj = new Object();
obj.name = name,
obj.age = age,
obj.year = function() {
console.log(this.name + 'は今年' + this.age + '歳です!')
}
return obj;
}
var obj = createObj('小明', 22);
obj.year();
}
// 小明は今年22歳です!
利点:オブジェクトの重複インスタンス化の問題を解決
欠点:オブジェクトのタイプを識別できない、すべてのインスタンスが同じプロトタイプを指す
3. コンストラクタ関数#
window.onload = function() {
function Person(name, age) {
this.name = name;
this.age = age;
this.year = function() {
console.log(this.name + 'は今年' + this.age + '歳です!')
}
}
var student = new Person('小明', 22);
student.year();
}
// 小明は今年22歳です!
利点:オブジェクトのタイプを識別できる
欠点:複数のインスタンスがメソッドを重複して作成し、共有できない
4. プロトタイプパターン#
window.onload = function() {
function Par() {}
Par.prototype = {
constructor: 'Par',
name: '小明',
age: 22,
year: function() {
console.log(this.name + 'は今年' + this.age + '歳です!')
}
};
var son = new Par();
son.year();
}
// 小明は今年22歳です!
欠点:すべてのインスタンスがその属性とメソッドを共有し、引数を渡したり属性値を初期化したりできない
5. 混合パターン(推奨使用)#
これはコンストラクタ関数とプロトタイプパターンを混合した書き方で、それぞれの利点を持ち、コンストラクタ関数はインスタンス属性を共有し、プロトタイプパターンはメソッドと共有したい属性を共有し、引数を渡したり属性値を初期化したりできます。
まずコンストラクタ関数でオブジェクトの属性メソッドを定義し、その後プロトタイプパターンでメソッドを作成します。使用する属性は prototype を通じて取得し、constructor 属性があり、操作する関数オブジェクト(コンストラクタ)を指すことができます。
例えばconstructor: Par
は、以下のプロトタイプメソッドがPar()
オブジェクト(コンストラクタ)を指すことを示します。
window.onload = function() {
function Par(name, age) {
this.name = name;
this.age = age;
}
Par.prototype = {
constructor: Par,
year: function() {
console.log(this.name + 'は今年' + this.age + '歳です!');
}
};
var son = new Par('小明', 22)
son.year();
}
// 小明は今年22歳です!
三、プロトタイプ、プロトタイプチェーン#
1. プロトタイプオブジェクト#
- 関数オブジェクトはすべて
prototype
属性を持ち、これは関数のプロトタイプオブジェクト(ブラウザメモリで作成されたオブジェクト)を指します。プロトタイプオブジェクトはすべてconstructor
属性を持ち、これはprototype
属性が存在する関数オブジェクト(コンストラクタ)を指します。
window.onload = function() {
function Par(name, age) {
this.name = name;
this.age = age;
}
Par.prototype = {
// constructorはオブジェクトを指す
constructor: Par,
year: function() {
console.log(this.name + 'は今年' + this.age + '歳です!');
}
};
var son = new Par('小明', 22)
son.year();
/*********************************************/
console.log(Par.prototype)
console.log(Par.prototype.constructor)
/*********************************************/
}
コンソールを通じて確認できます。
コンストラクタのprototype
属性はプロトタイプオブジェクトを指し、プロトタイプオブジェクトのconstructor
属性はコンストラクタを指します。
- コンストラクタ関数を呼び出してインスタンスを作成すると、そのインスタンスには隠れた属性
__proto__
があり、これはコンストラクタのプロトタイプオブジェクトを指します。
console.log(son.__proto__ === Par.prototype)
// true
- すべてのコンストラクタの prototype は object 型です。
console.log(typeof Par.prototype)
// object
- Function の prototype は空の関数であり、すべての組み込み関数の__proto__属性はこの空の関数を指します。
console.log(Math.__proto__)
- もしコンストラクタインスタンスとプロトタイプオブジェクトの両方に同じ属性が定義されている場合、呼び出すとプロトタイプオブジェクトの属性が隠されます。プロトタイプオブジェクトの属性値にアクセスしたい場合は、同名の属性をインスタンス(コンストラクタ)から完全に削除する必要があります。
window.onload = function () {
function Par(name) {
this.name = name;
}
Par.prototype.name = "張三";
var son = new Par("李四");
console.log(son.name); // 李四
console.log(son.__proto__.name); // 張三
// deleteを使用してインスタンスの同名属性値を削除
console.log(delete son.name); // true
console.log(son.name); // 張三
}
hasOwnProperty(属性名)
を使用して、属性がコンストラクタに存在するか、プロトタイプオブジェクトに存在するかを判断できます。
true
はコンストラクタに存在することを示し、false
はプロトタイプオブジェクトに存在することを示します。
console.log(Par.hasOwnProperty(name)); // false
- 演算子
in
を使用して、属性が存在するかどうかを判断できます(コンストラクタとプロトタイプオブジェクトの両方に存在する場合も可)。
window.onload = function () {
function Par(name, age) {
this.name = name;
this.age = age;
}
Par.prototype = {
constructor: Par,
year: function() {
console.log(this.name + this.age)
}
};
var son = new Par('xm', '22')
son.year();
console.log('name' in Par); // true
console.log('age' in Par); // false
}
同じ 2 つの属性が、インスタンスまたはプロトタイプオブジェクトに存在するかどうかを判断すると、出力結果が異なります。
参考:《オブジェクトに特定の属性があるかどうか in》https://www.cnblogs.com/IwishIcould/p/12333739.html
2.__proto__と prototype の違い#
-
prototype
属性は関数オブジェクトにのみ存在し、__proto__
属性はすべてのオブジェクトに存在します。 -
prototype
は関数オブジェクトがプロトタイプオブジェクトを指し、__proto__
はインスタンスが関数オブジェクトのプロトタイプオブジェクトを指します。 -
プロトタイプチェーンは、親タイプのインスタンスを子タイプのプロトタイプオブジェクトとして使用するこのチェーン状の関係を
プロトタイプチェーン
と呼びます。
3. 継承#
- プロトタイプチェーン継承
利点:親クラスのプロトタイプで定義された属性とメソッドを再利用できる
欠点:子クラスのインスタンスには独自の属性がなく、親クラスにパラメータを渡すことができない
function test1() {
function SuperType() {
this.city = [ "北京", "上海", "天津" ];
this.property = true;
}
SuperType.prototype = {
constructor : SuperType, // コンストラクタとプロトタイプオブジェクトの整合性を保つ
age : 15,
getSuperValue : function() {
return this.property;
}
};
function SonType() {
this.property = false;
}
// 子クラスのプロトタイプを親クラスのインスタンスに再設定:親クラスのプロトタイプを継承
SubType.prototype = new SuperType();
SubType.prototype = {
constructor : SubType,
getSonType : function() {
return this.property;
}
};
// 利点の検証
let son = new SubType();
console.log(son.age); // 15
console.log(son.getSuperValue()); // false
// 欠点の検証
let instance1 = new SubType();
instance1.city.push("重慶");
console.log(instance1.city); // ["北京", "上海", "天津", "重慶"]
let instance2 = new SubType();
console.log(instance2.city); // ["北京", "上海", "天津", "重慶"]
}
// test1();
- コンストラクタ継承
利点:子クラスのインスタンスには独自の属性があり、親クラスにパラメータを渡すことができ、プロトタイプチェーン継承の欠点を解決
欠点:親クラスのプロトタイプの属性とメソッドは再利用できない
function test2() {
function SuperType(name) {
this.name = name;
this.city = [ "北京", "上海", "天津" ]
}
SuperType.prototype = {
constructor : SuperType,
age : 18,
showInfo : function() {
return this.name;
}
};
function SubType() {
// 親クラスをcall()またはapply()メソッドで呼び出し、子クラスのインスタンス属性を継承
SuperType.call(this, "張三");
}
// 利点の検証
let instance = new SubType();
instance.city.push("重慶");
console.log(instance.city); // ["北京", "上海", "天津", "重慶"]
let instance1 = new SubType();
console.log(instance1.city); // ["北京", "上海", "天津"]
// 欠点の検証
console.log(instance.age); // undefined
instance.showInfo(); // son.showInfo is not a function
}
// test2();
- 组合继承(推奨)
利点:プロトタイプの属性とメソッドを再利用でき、各子クラスのインスタンスには独自の属性がある
欠点:親クラスのコンストラクタが 2 回呼び出され、子クラスのプロトタイプ内の親クラスのインスタンス属性が子クラスのインスタンスによって上書きされる
function test3() {
function SuperType(name) {
this.name = name;
this.city = [ "北京", "上海", "天津" ]
}
SuperType.prototype = {
constructor : SuperType,
showInfo : function() {
console.log(this.name + "は今年" + this.age + "歳です");
}
};
function SubType(name, age) {
// 1. コンストラクタメソッドを通じてインスタンス属性の継承を実現
SuperType.call(this, name);
this.age = age;
}
// 2. プロトタイプチェーン継承を通じてプロトタイプメソッドの継承を実現
SubType.prototype = new SuperType();
// 利点の検証
let instance = new SubType("張三", 15);
instance.showInfo(); // 張三は今年15歳です
let instance1 = new SubType();
instance1.city.push("重慶");
console.log(instance1.city); // ["北京", "上海", "天津", "重慶"]
let instance2 = new SubType();
console.log(instance2.city); // ["北京", "上海", "天津"]
}
// test3();
- 寄生组合継承(推奨)
利点:组合継承の欠点を解決し、効率が高い
欠点:基本的にない
function test4() {
function inheritPrototype(subType, superType) {
// 1. 親クラスのプロトタイプを継承
var prototype = Object.create(superType.prototype);
// 2. 汚染されたconstructを再設定
prototype.constructor = subType;
// 3. 子クラスのプロトタイプを再設定
subType.prototype = prototype;
}
function SuperType(name) {
this.name = name;
this.city = [ "北京", "上海", "天津" ];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
};
function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
}
// 親クラスのプロトタイプを子クラスに指す
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function() {
console.log(this.age);
}
// 利点の検証
let instance = new SubType("張三", 15);
instance.sayName(); // 張三
let instance1 = new SubType();
instance1.city.push("重慶");
console.log(instance1.city); // ["北京", "上海", "天津", "重慶"]
let instance2 = new SubType();
console.log(instance2.city); // ["北京", "上海", "天津"]
}
// test4();
4.ES6 新メソッド--class#
新しいキーワードclass
は es6 から JavaScript に導入され、class
の目的はクラスの定義を簡単にすることです。
関数メソッドを使用して実装:
function Person(name) {
this.name = name;
}
Person.prototype.hello = function () {
console.log('こんにちは、' + this.name + 'さん!');
}
var son = new Person('xm')
son.hello(); // こんにちは、xmさん!
class
を使用して実装:
class Person {
constructor(name) {
this.name = name;
}
hello() {
console.log('こんにちは、' + this.name + 'さん!');
}
}
var son = new person('xm')
son.hello(); // こんにちは、xmさん!
見ての通り、class
の定義では、コンストラクタconstructor
属性と原型オブジェクト上の関数hello()
メソッドが直接含まれており、function
キーワードが省略されています。
注意が必要なのは、従来の書き方では、コンストラクタと原型オブジェクトが分散して書かれていましたが、class
を使用すると、両者を一つのオブジェクトにまとめることができ、最後の引数とメソッド呼び出しの書き方は同じです。
class の継承
class
を使用してオブジェクトを定義するもう一つの大きな利点は、継承がより便利になることです。Person
からPrimaryPerson
を派生させるために必要なコード量を考えてみてください。今では、プロトタイプ継承の中間オブジェクトやプロトタイプオブジェクトのコンストラクタなどを考慮する必要がなく、extends
を使用して直接実現できます。
class PrimaryPerson extends Person {
constructor(name, grade) {
super(name); // 親クラスのコンストラクタを呼び出すことを忘れないでください!
this.grade = grade;
}
myGrade() {
alert('私は' + this.grade + '年生です');
}
}
PrimaryPerson
の定義もclass
キーワードを使用して実現されており、extends
はプロトタイプチェーンオブジェクトがPerson
から来ていることを示しています。子クラスのコンストラクタは親クラスとは異なる場合があります。
例えば、PrimaryPerson
はname
とgrade
の 2 つのパラメータを必要とし、super(name)
を使用して親クラスのコンストラクタを呼び出す必要があります。そうしないと、親クラスのname
属性が正常に初期化されません。
PrimaryPerson
は自動的に親クラスPerson
のhello
メソッドを取得し、子クラスで新しいmyGrade
メソッドを定義しました。
ES6 で導入されたclass
と従来のJavaScriptプロトタイプ継承
にはどのような違いがあるのでしょうか?
実際には、何の違いもありません。class
の役割は、JavaScript エンジンに元々自分たちで書かなければならなかったプロトタイプチェーンコードを実装させることです。簡単に言えば、class
を使用する利点は、プロトタイプチェーンコードを大幅に簡素化できることです。
しかし!
現在、すべてのブラウザがclass
をサポートしているわけではないため、選択する際には慎重に行う必要があります!