オブジェクト指向言語の3大特徴の1つである「継承」。継承はJSにおいて非常に重要な概念であり、プログラマーのレベルを見る上で最も重要な指標の一つです。
プロトタイプ
すべてのコンストラクタはprototypeと呼ばれるプロパティを持っています。新しいプロパティやメソッドは prototype を使って追加することができ、追加されたプロパティはコンストラクタのすべてのオブジェクトに共通です。
function Student(name,age){
this.name = name;
this.age = age;
}
Student.prototype.study = function(){
console.log(`${this.name}勉強になる!`);
};
let stu1 = new Student('tom',20);
let stu2 = new Student('jack',22);
let stu3 = new Student('chris',25);
stu1.study();// tom勉強中
stu2.study();// jack勉強中
stu3.study();// chris勉強中
stu1.study === stu2.study === stu3.study;// true
上記のコードを通して、このコンストラクタによって生成されたオブジェクトはデフォルトでこのプロパティにリンクされることがわかります。また、このメソッドを追加することで、prototypeはオブジェクトのプロパティであり、そのプロパティ値はprototypeオブジェクトと呼ばれるオブジェクトであることがわかります。
プロトタイプの役割
- オブジェクト間のデータ共有
- クラスに新しいプロパティとメソッドを追加し、新しい内容は現在のページで既に作成されているオブジェクトに対しても有効です。
システム・クラスへのプロパティとメソッドの追加
プロトタイプを使用すると、システム・クラスにプロパティやメソッドを追加して拡張することができます。
// Array配列クラスにプロパティを追加する。
Array.prototype.type = 'Array';
// Arrayクラスに自身の長さを出力するメソッドを追加する。
Array.prototype.size = function(){
return this.length;
}
let arr = [1,2,3,4,5];
arr.type;// "Array"
arr.size();// 5
もちろん、上記のコードは単にプロトタイプ関数を示すためのものであり、以下の形式は、現在の配列内のすべての数値の最大値を取得するために使用される**max()**メソッドを追加するためのArray配列クラスのためのものです。
Array.prototype.max = function(){
if(!this.length) return NaN;
let max = this[0];
for( let i=1;i<this.length;i++ ){
if(typeof this[i] !== 'number') return NaN;
if(max<this[i]){
max = this[i];
}
}
return max;
}
プロトタイプとプロト
オブジェクトのプロトタイプは、実際には隠された属性である、つまり、オブジェクトが自分のプロトタイプのビューを見つけたい非常に不便ですが、この問題を解決するために、各オブジェクトのブラウザは、プロパティを提供します:プロト。
function Animal(){}
let cat = new Animal();
cat.__proto__;// オブジェクトのプロトタイプに直接アクセスする
protoは、オブジェクトのプロトタイプ・オブジェクトをオブジェクトの視点から論じたものです。prototypeは 、コンストラクタから見たプロトタイプ属性、つまりコンストラクタによって生成されたオブジェクトのプロトタイプ・オブジェクトを表します。注:実際には、コンストラクタのプロトタイプとオブジェクトのプロトタイプは同じものです。
cat.__proto__ === Animal.prototype;// true
注:protoプロパティはブラウザが提供するものであり、標準のプロパティではないため、Objectクラスのstaticメソッド:Object.getPrototypeOf();を使用するのが開発上の正しい方法です。
Object.getPrototypeOf(cat) === Animal.prototype;// true
静的メソッドとインスタンス・メソッド
すべてのクラスで両方のメソッドを持つことができます:
- 静的メソッド: クラスに直接バインドされたメソッドで、クラスから直接呼び出されます。
- Instance Methods(インスタンスメソッド):クラスのプロトタイプにバインドされたメソッドで、そのクラス配下のオブジェクトから呼び出されます。
静的メソッド
例えば
// パラメータが配列かどうかを判定する
Array.isArray();
// オブジェクトのkey
Object.keys();
// オブジェクトのプロトタイプを取得する
Object.getPrototypeOf();
...
静的メソッドと呼ばれるものは、実際にはオブジェクトからではなく「クラス」から呼び出されます。あなた自身のクラスと静的メソッドを定義してください。
//
function Str(){
}
// strクラスのスタティック・メソッド
Str.getSize = function(args){
return args.length;
}
Str.getSize('hello');// 5
インスタンスメソッド
インスタンスメソッドは、開発で最も使用されます。
// 文字列中の指定文字の出現回数を取得する
String.prototype.getCharCount(char){
let reg = new RegExp(char,'g');
let count = 0;
while(Array.isArray(reg.exec(this))==true){
count++;
}
return count;
}
'hello'.getCharCount('l');// 2
コンストラクタ
すべてのオブジェクトには、コンストラクタを記述するコンストラクタ属性があります。
function Student(){}
let stu = new Student();
stu.prototype.constructor; // function Student(){}
コンストラクタ属性はオブジェクトのプロトタイプ・オブジェクトによって提供されるため、オブジェクトはこの属性に直接アクセスできます。これにより、関数の "name" 属性から現在のオブジェクトの "class" 名を取得することができます。
stu.constructor.name;// Student
プロトタイプチェーン
JSのすべてのオブジェクトは、それ自身のプロトタイプ・オブジェクトを持ち、プロトタイプ・オブジェクトは、それ自身のプロトタイプ・オブジェクトを持ちます。このように1つレベルを上げると、すべてのオブジェクトのプロトタイプは、Objectコンストラクタのプロトタイプ・プロパティであるObject.prototypeまで遡ることができます。つまり、すべてのオブジェクトはObject.prototypeのプロパティを継承しています。これは、すべてのオブジェクトがvalueOfメソッドとtoStringメソッドを持っている理由も説明します。
このロジックに従うと、Object.prototypeオブジェクトのプロトタイプまで進みますが、そのプロトタイプはnullで終わり、nullにはプロパティがないので、プロトタイプの連鎖はそこで終わります。
function Student(){}
let stu = new Student();
console.log(Student.prototype);
console.log(Object.getPrototypeOf(Student.prototype));
console.log(Object.getPrototypeOf(Student.prototype).constructor);// Object
console.log(Object.getPrototypeOf(Object.getPrototypeOf(Student.prototype)));// null
プロトタイプ・チェインの継承を使用すると、子クラスが親クラスの能力を獲得するような操作を行うことができます。
梱包対象物
JS言語でメソッドを呼び出せるのはオブジェクトだけです。
// 配列オブジェクトを作る
let arr = [];
// 配列オブジェクト呼び出しメソッド
arr.push('hello');
arr.push('JS');
arr;// ['hello','JS']
しかし、基本データ型の値はオブジェクトではありませんが、例えばメソッドを呼び出すことができるということは興味深いことです:
let str = 'hello JS';
str.charAt(4);// 'o'
let num = 10;
num.toString();// '10'
そこで登場するのが、オブジェクトのラッピングです。
いわゆる「ラップ・オブジェクト」とは、Number、Boolean、Stringの3つのネイティブ・オブジェクトに対応する3種類の値のことです。これらの3つのネイティブオブジェクトは、元の型の値をオブジェクトに変換することができます。
直接文字列呼び出しメソッドを使用して実際には、JSの内部でいくつかのことを行うには、静かに文字列オブジェクトに文字列を変換します。文字列メソッドの呼び出しの終了時に、JSは静かに元の値型Stringに文字列オブジェクトを変換します。 具体的な手順は次のとおりです:
'hello world'.indexOf('o');
// JS以下の操作は内部的に実行される。
// 1文字列値を文字列オブジェクトに変換するには、再帰を使う必要がある。
let str = new String('hello world');
str;// String{"hello world"}
// 2オブジェクトはメソッド
str.indexOf('o');
//3以下は、文字列オブジェクトを基本データ型に再帰的にコピーする例である。
let str = new String('hello world').valueOf();
str;// 'hello world'
上記のコードの後、JSは非常に勤勉であることを感じませんか?はい、JS言語はしばしば骨の折れる仕事のいくつかを完了するのに役立ちますが、このメカニズムは諸刃の剣であり、心に留めておく必要があります。このメカニズムを理解することは努力を節約しますが、そうでなければ、それは "ピット "になります。
一般的な相続の種類
JSの継承は、作業効率を向上させるための非常に実用的かつ効率的な手段です。いくつかの一般的な継承を学び、探求するための研究と開発のために、次のとおりです。
- プロトタイプ継承
- コンストラクタ継承
- コピー継承
- hasOwnProperty():属性の検出
プロトタイプ継承
プロトタイプ継承とは、親クラスのインスタンスを子クラスのプロトタイプとして使用することです。
//
function Person(name){
this.name = name||'tt';
this.sayHello = function(){
alert('hello');
}
}
Person.prototype.sayGoodbye = function(){
alert('goodbye');
}
//
function Student(){}
// サブクラスのプロトタイプを変更する
Student.prototype = new Person();
let stu = new Student();
stu.name;// tt
Student.prototype.name = 'jack';
stu.name;// 'jack'
stu.sayHello();
stu.sayGoodbye();
コンストラクタ継承
コンストラクタ型継承では、主に **call() と apply()** メソッドを使用します。コンストラクタ型継承を理解するためには、まずこの2つのメソッドがどのようなものかを理解する必要があります。
JavaScriptのすべてのFunctionオブジェクトには、apply()メソッドとcall()メソッドがあります。
- apply():オブジェクトのメソッドを呼び出し、現在のオブジェクトを別のオブジェクトに置き換えます。例:B.apply(A, arguments); つまり、AオブジェクトはBオブジェクトのメソッドを適用します。
- call():メソッドのオブジェクトを呼び出し、現在のオブジェクトを別のオブジェクトで置き換えます。つまり、AオブジェクトはBオブジェクトのメソッドを呼び出します。
/*apply() */
function.apply(thisObj[, argArray])
/*call() */
function.call(thisObj[, arg1[, arg2[, [,...argN]]]]);
もちろん、上記の基本的な説明とコードだけでは、まだ何をするのか理解できないでしょう。ここではその基本的な使い方を見てみましょう。
//
function getSum(num1,num2){
return num1+num2;
}
//
function getDiff(num1,num2){
return num1-num2;
}
// getDiffgetSumを呼び出す。
getSum.apply(getDiff,[10,5]);// 15
// getSum getDiff
getDiff.apply(getSum,[10,5]);// 5
// getDiffgetSumを呼び出す。
getSum.call(getDiff,10,5);// 15
// getSum getDiff
getDiff.call(getSum,10,5);// 5
上記のコードから見つけることができる、これらの2つの関数の特性は、呼び出し元の呼び出し()または適用()メソッドの実際の実装の外側の内部です。図これらの2つの関数を使用する方法をコンストラクタの継承の必要性の後に達成することができます。
//
function Person(name){
this.name = name||'tt';
this.sayHello = function(){
alert('hello');
}
}
Person.prototype.sayGoodbye = function(){
alert('goodbye');
}
//
// 親クラスのコンストラクタを使って子クラスを拡張することは、親クラスのインスタンス属性を子クラスに代入することと同じである。
function Student(name){
// call()メソッドを使うことで、これを
Person.call(this);
this.name = name;
}
let stu = new Student('jack');
stu.name;// 'jack'
stu.sayHello();
stu.sayGoodbye();
applyメソッドについては、さらにいくつかのトリックがあります。
// 数学オブジェクトのmaxメソッドを使って、配列の最大数を素早く求める
let arr = [,8,,21,14];
// 最初のパラメータnullは、オブジェクトのthis
console.log(Math.max.apply(null,arr));
call()メソッドとapply()メソッドの違い
どちらのメソッドも、現在のオブジェクトを別のオブジェクトに置き換えます。この2つのメソッドの違いは、主にパラメータにあります。
- call(): 引数は無制限
- パラメータ 1: 新しい this オブジェクト。
- パラメータ2,3,4...その他のパラメータ
- apply():引数は 2 つだけです。
- パラメータ 1: 新しい this オブジェクト
- 引数2: 配列または配列に似たオブジェクト
コピー継承
コピー継承もよく使われますが、コピーには区別があります:
- 浅いコピー:直接割り当てられたコピー。
- ディープ・コピー:オブジェクトAのすべての属性をオブジェクトBにコピーします。
シャローコピー
let obj1 = {
name:'tom',
age:20
};
// 浅いコピー、直接代入コピー
let obj2 = obj1;
obj2.height = '180cm';
obj1;// {name:'tom',age:20,height:'180cm'}
obj2;// {name:'tom',age:20,height:'180cm'}
その通り、いわゆるシャロー・コピーは、あなたが学んだ配列の直接代入と同じことで、実際にはobj1のアドレス値をobj2に渡しているのです。
ディープコピー
let obj1 = {
name:'tom',
age:20
};
// ディープ・コピーは、オブジェクトAのすべての属性をオブジェクトBにコピーする。
let obj2 = {};
for( let key in obj1 ){
obj2[key] = obj1[key]
}
obj2.height = '180cm'
console.log(obj1);// {name: "tom", age: 20}
console.log(obj2);// {name: "tom", age: 20, height: "180cm"}
ディープコピーの利点は、obj2に対する操作がobj1オブジェクトに影響しないことですが、上のコードは実はバグだらけです。
let obj1 = {
name:'tom',
age:20,
hobby:['game','eat']
};
// ディープ・コピーは、オブジェクトAのすべての属性をオブジェクトBにコピーする。
let obj2 = {};
for( let key in obj1 ){
obj2[key] = obj1[key]
}
obj2.hobby.push('basketball');
console.log(obj1.hobby);// ['game','eat','basketball']
console.log(obj1.hobby);// ['game','eat','basketball']
オブジェクトobj1の属性の値が基本データ型でなくなった場合、単純なコピーではやはり他人のアドレスの値を直接運ぶことになるので、直接単純にコピーすることはできません。そのため、もう少し厳密な処理が必要となり、再帰を使用することができます。
let obj1 = {
name:'tom',
age:20,
hobby:['game','eat']
};
// 再帰関数を定義する
function deepClone(obj){
let objClone = Array.isArray(obj)?[]:{};
if( obj && typeof obj==='object' ){
for( let key in obj ){
// 参照データ型であれば、再帰を使う。
if(obj[key] && typeof obj[key]==='object'){
objClone[key] = deepClone(obj[key]);
}else{
// 基本データ型を直接コピーする場合
objClone[key] = obj[key]
}
}
}
return objClone;
}
let obj2 = deepClone(obj1);
obj2.hobby.push('haha');
console.log(obj1);
console.log(obj2);