在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採用人為約定的方式,去間接實現私有;
在屬性和方法之前新增下劃線_
,約定這樣的屬性和方法,只能在類中可見,只能靠人為遵守這樣的約定;
我們知道,可以用typeof
關鍵字來獲取一個變數是什麼資料型別;
現在可以用instanceof
關鍵字,來判斷一個物件是什麼類的範例;
語法obj instanceof className
,會返回一個布林值:
className
是obj
原型鏈上的類,返回true;它是怎麼去判斷的呢?假設現在有如下幾個類:
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]
的,如果有一個類實現了這個靜態方法,那麼instanceof關鍵字會直接呼叫這個靜態方法;
如果類沒有實現這個靜態方法,那麼則會按照上述說的流程去檢查;
class className {
static [Symbol.hasInstance]() {};
}
isPrototypeOf()
方法,會判斷objA
的原型是否處在objB
的原型鏈中,如果在則返回true,否則返回false;
objA.isPrototypeOf(objB)
就相當於objB instanceof classA
;
反過來,objB instanceof classA
就相當於classA.prototype.isPrototypeOf(objB)
;
我們知道,JS的繼承,是通過原型來實現的,現在結合原型來說一下類的繼承相關內容。
JS中表示繼承的關鍵字是extends
,如果classA extends classB
,則說明classA
繼承classB
,classA
是子類,classB
是父類別;
時刻記住,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屬性,預設就有兩個屬性:
以及
從上圖中可以看出,A
的prototype屬性裡,除構造器和原型以外,就只有一個普通方法show()
;
這說明,只有類的普通方法,會自動進入類的prototype
屬性參與繼承;
也就是說,一個類物件的資料結構,如下:
另外,類的prototype
屬性是不可寫的,但是類物件的原型則是可以修改的;
當子類去繼承父類別的時候,到底繼承到了父類別的哪些東西,也即子類可以用父類別的哪些內容;
從結果上來看,我們可以確定如下:
由於原型鏈的存在,這些繼承會一路沿著原型鏈回溯,繼承到所有祖宗類;
由於繼承的機制,勢必子類和父類別可能會有同名屬性的存在:
從結果上可以看到,雖然子類直接將父類別的普通屬性作為自己的普通屬性,但是當出現同名屬性,屬性值會進行覆蓋,最終的值採用子類自己定義的值;
與屬性一樣,子類和父類別也可能會出現同名方法;
當然大多數情況下,是我們自己要拓展方法功能而故意同名,從而重寫父類別的方法;
如上所示,我們重寫了父類別的靜態方法和普通方法;
如果是重寫構造器的話,分兩種情況:
// 基礎類別重寫構造器
class A {
constructor() {
code...
}
}
// 派生類重寫構造器
class B extends A() {
constructor() {
// 一定要先寫super()
super();
code...
}
}
從上圖還可以看出來,子類呼叫方法的順序:
類的方法裡,有一個特殊的、專門用於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
;
終於說到構造器了,理解了構造器的具體建立物件的過程,我們就能理解關於繼承的很多內容了;
先來看一下基礎類別的構造器建立物件的過程:
執行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
中有賦值,則暫存這個值;