オブジェクト指向は、問題を構成するトランザクションを各オブジェクトに分解し、オブジェクトを構築する目的は、ステップを完了するためではなく、全体の問題を解決する過程で発生する行動を記述することにあります。これは、汎用コードを記述し、コードの再利用を強化し、差異を隠すことを意図しています。
一、オブジェクト指向プログラミングとは#
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
}
同じ属性がインスタンスまたはプロトタイプオブジェクトに存在するかどうかを判断すると、出力結果が異なります。
参考:《オブジェクトに特定の属性があるかどうか 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()メソッドで呼び出し、子クラスと同じthisを共有し、子クラスのインスタンス属性の継承を実現
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. 汚染されたconstructorを再設定
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
をサポートしているわけではないため、選択する際には慎重に行う必要があります!