在JS中,類是后來才出的概念,早期創造物件的方式是new Function()呼叫建構式創建函式物件;
而現在,可以使用new className()構造方法來創建類物件了;
所以在很多方面,類的使用方式,很像函式的使用方式:
但是類跟函式,還是有本質區別的,這在原型那里已經說過,不再贅述;
如何定義一個類
如下所示去定義一個類:
class className {
// 屬性properties
property1 = 1;
property2 = [];
peoperty3 = {};
property4 = function() {};
property5 = () => {};
// 構造器
constructor(...args) {
super();
// code here
};
// 方法methods
method1() {
// code here
};
method2(...args) {
//code here
};
}
可以定義成員屬性和成員方法以及構造器,他們之間都有封號;隔開;
在通過new className()創建物件obj的時候,會立即執行構造器方法;
屬性會成為obj的屬性,句式為賦值陳述句,就算等號右邊是函式,它也依然是一個屬性,注意與方法宣告陳述句區別開;
方法會成為obj的原型里的方法,即放在className.prototype屬性里;
像使用function一樣使用class關鍵字
正如函式運算式一樣,類也有類運算式:

還可以像傳遞一個函式一樣,去傳遞一個類:

這在Java中是不可想象的,但是在JS中,就是這么靈活;
靜態屬性和靜態方法
靜態屬性和靜態方法,不會成為物件的屬性和方法,永遠都屬于類本身,只能通過類去呼叫;
-
定義語法
// 直接在類中,通過static關鍵字定義 class className { static property = ...; static methoed() {}; } // 通過類直接添加屬性和方法,即為靜態的 class className {}; className.property = ...; className.method = function() {}; -
呼叫語法
類似于物件呼叫屬性和方法,直接通過類名去呼叫
className.property; className.method();
靜態屬性/方法,可以和普通屬性/方法同名,這不會被弄混,因為他們的呼叫者不一樣,前者是類,后者是類物件;
私有屬性和私有方法
JS新增的私有特性,在屬性和方法之前添加#號,使其只在類中可見,物件無法呼叫,只能通過類提供的普通方法去間接訪問;
-
定義和呼叫語法
class className { // 定義,添加#號 #property = ...; #method() {}; // 只能在類中可見,呼叫也需要加#號 getProperty() { return this.#property; } set property(value) { this.#property = value; } }
注意,#property是一個總體作為屬性名,與property是不同的,#method同理;
在這個私有特性之前,JS采用人為約定的方式,去間接實作私有;
在屬性和方法之前添加下劃線_,約定這樣的屬性和方法,只能在類中可見,只能靠人為遵守這樣的約定;
類檢查instanceof
我們知道,可以用typeof關鍵字來獲取一個變數是什么資料型別;
現在可以用instanceof關鍵字,來判斷一個物件是什么類的實體;
語法obj instanceof className,會回傳一個布林值:
- 如果
className是obj原型鏈上的類,回傳true; - 否則,回傳false;
它是怎么去判斷的呢?假設現在有如下幾個類:
class A {};
class B extends A {};
class C extends B {};
let c = new C();
c的原型是C.prototype;
C.prototype的原型是B.prototype;
B.prototype的原型是A.prototype;
A.prototype的原型是Object.prototype;
Object.prototype的原型是null;
原型鏈如上所示;
當我們執行c instanceof A的時候,它是這樣的程序:
c.__proto__ === A.prototype?否,則繼續;
c.__proto__.__proto__ === A.prototype?否,則繼續;
c.__proto__.__proto__.__proto__ === A.prototype?是,回傳true;
如果一直否的話,這個程序會持續下去,直到將c的原型鏈溯源到null,全都不等于A.prototype,則回傳false;
也就是說,instanceof關鍵字,比較的是物件的原型鏈上的原型和目標類的prototype是否相等(原型和prototype里有constructor,但是instanceof不會比較構造器是否相等,只會比較隱藏屬性[[Prototype]]);
靜態方法Symbol.hasInstance
大多數類是沒有實作靜態方法[Symbol.hasInstance]的,如果有一個類實作了這個靜態方法,那么instanceof關鍵字會直接呼叫這個靜態方法;
如果類沒有實作這個靜態方法,那么則會按照上述說的流程去檢查;
class className {
static [Symbol.hasInstance]() {};
}
objA.isPrototypeOf(objB)
isPrototypeOf()方法,會判斷objA的原型是否處在objB的原型鏈中,如果在則回傳true,否則回傳false;
objA.isPrototypeOf(objB)就相當于objB instanceof classA;
反過來,objB instanceof classA就相當于classA.prototype.isPrototypeOf(objB);
繼承
我們知道,JS的繼承,是通過原型來實作的,現在結合原型來說一下類的繼承相關內容,
關鍵字extends
JS中表示繼承的關鍵字是extends,如果classA extends classB,則說明classA繼承classB,classA是子類,classB是父類;
原型高于extends
時刻記住,JS的繼承,是依靠原型來實作的;
關鍵字extends雖然確立了兩個類的父子關系,但是這只是一開始確立子類的父原型;
但是父原型是可以中途被修改的,此時子類呼叫方法,是沿著原型鏈去尋找的,而不是沿著子類父類的關鍵字宣告去尋找的,這和Java是不一樣的:

如圖所示,C extends A確立了C一開始的父原型是A.prototype,c.show()呼叫的也是父類A的方法;
但是后面修改c的父原型為B.prototype,c.show呼叫的就不是父類A的方法,而是父原型的方法;
也就是說,原型才是核心,高于extends關鍵字;
基類和派生類
class classA {};
class classB extends classA {};
像classA這樣沒有繼承任何類(實際上父原型是Object.prototype)的類稱為基類;
像classB這樣繼承classB的類,稱為classB的派生類;
為什么要分的這么細,是因為在創建類時,他們兩個的行為不同,后面會說到;
類的原型
類本身也是有原型的,就像類物件有原型一樣;

可以看到,B的原型就是其父類A,而A作為基類,基類的原型是本地方法;
正因如此,B可以通過原型去呼叫A的靜態方法/屬性;
也就是說,靜態方法/屬性,也是可以繼承的,通過類的原型去繼承;
類物件的原型和類的prototype屬性
在創建類物件的時候,會將類的prototype屬性值復制給類物件的原型;
所以說,類物件的原型等于類的prototype屬性值;

而類的prototype屬性,默認就有兩個屬性:
- 構造器constructor:指向類本身;
- 原型[[Prototype]]:指向父類的prototype屬性;
以及
- 類的普通方法;
從上圖中可以看出,A的prototype屬性里,除構造器和原型以外,就只有一個普通方法show();
這說明,只有類的普通方法,會自動進入類的prototype屬性參與繼承;
也就是說,一個類物件的資料結構,如下:
- 普通屬性
- (原型)prototype屬性
- 構造器
- 父類的prototype屬性(父原型)
- 方法
另外,類的prototype屬性是不可寫的,但是類物件的原型則是可以修改的;
繼承了哪些東西
當子類去繼承父類的時候,到底繼承到了父類的哪些東西,也即子類可以用父類的哪些內容;

從結果上來看,我們可以確定如下:
- 子類繼承父類的靜態屬性/方法(基于類的原型);
- 子類物件繼承父類的普通方法和構造器(基于類的prototype);
- 子類直接將父類的普通屬性作為自己的普通屬性(普通屬性不參與繼承);
由于原型鏈的存在,這些繼承會一路沿著原型鏈回溯,繼承到所有祖宗類;
同名屬性的覆寫
由于繼承的機制,勢必子類和父類可能會有同名屬性的存在:

從結果上可以看到,雖然子類直接將父類的普通屬性作為自己的普通屬性,但是當出現同名屬性,屬性值會進行覆寫,最終的值采用子類自己定義的值;
同名方法的重寫
與屬性一樣,子類和父類也可能會出現同名方法;
當然大多數情況下,是我們自己要拓展方法功能而故意同名,從而重寫父類的方法;

如上所示,我們重寫了父類的靜態方法和普通方法;
如果是重寫構造器的話,分兩種情況:
// 基類重寫構造器
class A {
constructor() {
code...
}
}
// 派生類重寫構造器
class B extends A() {
constructor() {
// 一定要先寫super()
super();
code...
}
}
子類的呼叫順序
從上圖還可以看出來,子類呼叫方法的順序:
- 先從自己的方法里呼叫,發現沒有可呼叫的方法時;
- 再沿著原型鏈,先從父類開始尋找方法,一直往上溯源,直到找到可呼叫的方法,或者沒有而出錯;
super關鍵字
類的方法里,有一個特殊的、專門用于super關鍵字的特殊屬性[[HomeObject]],這個屬性系結super陳述句所在的類的物件,不會改變;
而super關鍵字,則指向[[HomeObject]]系結的物件的類的父類的prototype;
這要求,super關鍵字用于派生類類的方法里,基類是不可以使用super的,因為沒有父類;
當我們使用super關鍵字時,借助于[[HomeObject]],總是能夠正確重用父類方法;

如上,super陳述句所在的類為B,其物件為b,即[[HomeObject]]系結b;
而super則指向b的類的父原型,即A的prototype屬性;
而super.show()就類似于A.prototype.show(),故而最終結果如上所示;
可以簡單理解成,super指向子類物件的父類的prototype;
構造器constructor
終于說到構造器了,理解了構造器的具體創建物件的程序,我們就能理解關于繼承的很多內容了;
先來看一下基類的構造器創建物件的程序:

執行let a = new A()時,大致流程如下:
- 首先呼叫
A.prototype的特性[[Prototype]]創建一個字面量物件,同時this指標指向這個字面量物件; - 然后執行類
A()的定義,A定義的普通屬性成為字面量物件的屬性并初始化,A.prototype的value值復制給字面量物件的隱藏屬性[[Prototype]]; - 然后再執行
constructor構造器,沒有構造器就算了; - 回傳
this指標給變數a,即a此時參考該字面量物件了;
從結果上看,在執行構造器時,字面量物件就已經有原型了,以及屬性name,且值初始化為tomA;
然后才對屬性name重新賦值為jerryA;
然而,構造器中對屬性的重新賦值,從一開始就決定好了,只是在執行到這句賦值陳述句之前,暫存在字面量物件中;
現在再來看一下派生類創建物件的程序;

執行let b = new B()的大致流程如下:
- 首先呼叫
B.prototype的特性[[Prototype]]創建一個字面量物件,同時this指標指向這個字面量物件; - 然后執行類
B()的定義,B定義的普通屬性成為字面量物件的屬性并初始化,B.prototype的value值復制給字面量物件的隱藏屬性[[Prototype]]; - 然后再執行
constructor構造器(沒有顯式定義構造器會提供默認構造器),第一句super(),開始進入類A()的定義;- 暫存
B的屬性值,轉而賦值為A定義的值,A.prototype的value值復制給B.__proto__的隱藏屬性[[Prototype]]; - 然后執行
constructor構造器(基類沒有構造器就算了); - 回傳
this指標; - 丟棄
A賦值的屬性值,重新使用暫存的B的屬性值;
- 暫存
- 繼續執行
constructor構造器剩下的陳述句; - 回傳
this指標給變數b,即b參考該字面量物件了;
通過基類和派生類創建物件的流程對比,可以發現主要區別在于類的屬性的賦值上;
屬性值從一開始就已經暫存好:
- 如果構造器
constructor中有賦值,則暫存這個值; - 如果構造器沒有,則暫存類定義中的值;
- 不管父類及其原型鏈上同名的屬性在中間進行過幾次賦值,最終都會重新覆寫為最開始就暫存好的值;
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/540684.html
標籤:其他
上一篇:第一百一十七篇: JavaScript 工廠模式和原型模式
下一篇:Web 標準 & W3C 規范
