原創文章&經驗總結&從校招到A廠一路陽光一路滄桑
詳情請戳www.coderccc.com
> 主要知識點:類聲明、類表達式、類的重要要點以及類繼承
1. 類的聲明
基本的類聲明
類聲明以class
關鍵字開始,其後是類的名稱;類中的方法就像是對象字面量中的方法簡寫,並且方法之間不需要使用逗號:
class PersonClass{
constructor(name){
this.name = name;
}
sayName(){
console.log(this.name);
}
}
let person = new PersonClass("hello class");
person.sayName();
類聲明語法允許使用constructor
直接定義一個構造器,而不需要先定義一個函數,再把它當做構造器來使用。類中的方法使用的函數簡寫語法,省略了關鍵字function
。
使用class關鍵字來定義一個類型,有這樣幾個要點:
- 類聲明不會被提升,這與ES6之前通過函數定義不同。類聲明與使用
let
定義一樣,因此也存在暫時性死區; - 類聲明中的所有代碼會自動運行在嚴格模式下,並且無法退出嚴格模式;
- 類的所有方法都是不可枚舉的;
- 類的所有內部方法都沒有
[[Constructor]]
,因此使用new
來調用他們會拋出錯誤; - 調用類構造器時不使用
new
,會拋出錯誤; - 試圖在類的內部方法中重寫類名,會拋出錯誤;
2. 類表達式
類與函數有相似之處,都有兩種形式:聲明與表達式。函數聲明與類聲明都以關鍵詞開始(分別是function和class),之後就是標識符(即函數名或者類名)。如果需要定義匿名函數,則function後面就無需有函數名,類似的,如果採用類表達式,關鍵是class後也無需有類名;
基本的類表達式
使用類表達式,將上例改成如下形式:
let PersonClass = class {
constructor(name){
this.name = name;
}
sayName(){
console.log(this.name);
}
}
let person = new PersonClass("hello class");
person.sayName(); //hello class
示例代碼中就定義了一個匿名的類表達式,如果需要定義一個具名的類表達式,只需要像定義具名函數一樣,在class關鍵字後面寫上類名即可。
3. 類的重要要點
作爲一級公民的類
在編程中,能夠被當作值來使用的就成爲一級公民(first-class citizen)。既然都當作值使用,就說明它能夠作爲參數傳遞給函數、能作爲函數的返回值也能用來給變量賦值。JS中的函數是一等公民,類也是一等公民。
例如,將類作爲參數傳遞給函數:
function createObj(classDef){
return new classDef();
}
let obj = createObj(class{
sayName(){
console.log('hello'); //hello
}
})
obj.sayName();
**類表達式另一個重要用途是實現立即調用類構造器以創建單例。**語法是使用new
來配合類表達式使用,並在表達式後面添加括號():
//立即調用構造器
let person = new class{
constructor(name){
this.name = name;
}
sayName(){
console.log(this.name);
}
}('hello world');
person.sayName(); //hello world
訪問器屬性
自有屬性需要在類構造器中創建,而類還允許創建訪問器屬性。爲了創建一個getter
,要使用get
關鍵字,並要與後面的標識符之間留出空格;創建setter
使用相同的方式,只需要將關鍵字換成set
即可:
class PersonClass{
constructor(name){
this.name = name;
}
get name(){
return name; //不要使用this.name會導致無限遞歸
}
set name(value){
name=value; //不要使用this.value會導致無限遞歸
}
}
let person = new PersonClass('hello');
console.log(person.name); // hello
person.name = 'world';
console.log(person.name); //world
let descriptor = Object.getOwnPropertyDescriptor(PersonClass.prototype,'name');
console.log('get' in descriptor); //true
需計算屬性名
對象字面量和類之間的相似點有很多,類方法與類訪問器屬性都能使用需計算屬性名的方式,語法與對象字面量中需計算屬性名一樣,都是使用方括號[]來包裹表達式:
//需計算屬性名
let methodName ='sayName';
let propertyName = 'name';
class PersonClass{
constructor(name){
this.name = name;
}
get [propertyName](){
return name;
}
set [propertyName](value){
name = value;
}
[methodName](){
return console.log(this.name);
}
}
let person = new PersonClass('hello world');
person.sayName(); //hello world
console.log(person.name); //hello world
生成器方法
在對象字面量中定義一個生成器:只需要在方法名前附加一個星號*
即可,這一語法對類同樣有效,允許將類的任意內部方法編程生成器方法:
//生成器方法:
class GeneClass{
*generator(){
yield 1;
yield 2;
}
}
let obj = new GeneClass();
let iterator = obj.generator();
console.log(iterator.next().value); //1
console.log(iterator.next().value); //2
console.log(iterator.next().value); //undefined
可迭代對象用於Symbol.iterator
屬性,並且該屬性指向生成器函數。因此,在類定義中同樣可以使用Symbol.iterator
屬性來定義生成器方法,從而定義出類的默認迭代器。同時也可以通過生成器委託的方式,將數組、Set、Map等迭代器委託給自定義類的迭代器:
class Collection {
constructor() {
this.items = [];
}
*[Symbol.iterator]() {
for(let item of this.items){
yield item;
}
}
}
let collection = new Collection();
collection.items.push(1);
collection.items.push(2);
collection.items.push(3);
for (let x of collection) {
console.log(x);
}
輸出:1 2 3
靜態成員
ES6的類簡化了靜態成員的創建,只要在方法與訪問器屬性的名稱前添加static
關鍵字即可:
class PersonClass {
// 等價於 PersonType 構造器
constructor(name) {
this.name = name;
}
static create(name) {
return new PersonClass(name);
}
}
let person = PersonClass.create("Nicholas");
通過在方法前加上static
關鍵字,使其轉換成靜態方法。能在類中的任何方法與訪問器屬性上使用 static
關鍵字,唯一限制是不能將它用於 constructor
方法的定義。靜態成員不能用實例來進行訪問,始終需要用類自身才能訪問它們。
類繼承
使用關鍵字extends可以完成類繼承,同時使用super關鍵字可以在派生類上訪問到基類上的方法,包括構造器方法:
//類繼承
class Rec{
constructor(width,height){
this.width = width;
this.height = height;
}
getArea(){
return this.width*this.height;
}
}
class Square extends Rec{
constructor(width,height){
super(width,height);
}
}
let square = new Square(100,100);
console.log(square.getArea()); //10000
關於類繼承,還有這樣幾個要點:
- **在派生類中方法會覆蓋掉基類中的同名方法,**例如在派生類
Square
中有getArea()
方法的話就會覆蓋掉基類Rec
中的getArea()
方法; - 如果基類中包含了靜態成員,那麼這些靜態成員在派生類中也是可以使用的。注意:靜態成員只能通過類名進行訪問,而不是使用對象實例進行訪問;
從表達式中派生類
在ES6中派生類最大的能力就是能夠從表達式中派生類,只要一個表達式能夠返回的對象具有[[Constructor]]
屬性以及原型,你就可以對該表達式使用extends
進行繼承。由於extends後面能夠接收任意類型的表達式,這就帶來了巨大的可能性,可以動態決定基類,因此一種對象混入的方式:
//從表達式中派生類
let SerializableMixin = {
serialize() {
return JSON.stringify(this);
}
};
let AreaMixin = {
getArea() {
return this.length * this.width;
}
};
function mixin(...mixins) {
var base = function() {};
Object.assign(base.prototype, ...mixins);
return base;
}
class Square extends mixin(AreaMixin, SerializableMixin) {
constructor(length) {
super();
this.length = length;
this.width = length;
}
}
let x = new Square(3);
console.log(x.getArea()); // 9
console.log(x.serialize()); // "{"length":3,"width":3}"
mixin()
函數接受代表混入對象的任意數量的參數,它創建了一個名爲 base
的函數,並將每個混入對象的屬性都賦值到新函數的原型上。此函數隨後被返回,於是 Square
就能夠對其使用 extends
關鍵字了。注意由於仍然使用了 extends
,你就必須在構造器內調用 super()
。若多個混入對象擁有相同的屬性,則只有最後添加
的屬性會被保留。
4. 繼承內置對象
在E6中能夠通過extends
繼承JS中內置對象,例如:
class MyArray extends Array {
// 空代碼塊
}
let colors = new MyArray();
colors[0] = "red";
console.log(colors.length); // 1
colors.length = 0;
console.log(colors[0]); // undefined
Symbol.species
屬性Symbol.species
被用於定義靜態訪問器屬性,該屬性值用來指定類的構造器。當創建一個新的對象實例時,就需要通過Symbol.species
屬性獲取到構造器,從而新建對象實例。
下面內置對象都定義了Symbol.species
屬性:
- Array;
- ArrayBuffer;
- Map;
- Promise;
- RegExp;
- Set;
- 類型化數組
例如在自定義類型中,使用Symbol.species
:
class MyClass {
static get [Symbol.species]() {
return this;
}
constructor(value) {
this.value = value;
}
clone() {
return new this.constructor[Symbol.species](this.value);
}
}
class MyDerivedClass1 extends MyClass {
// 空代碼塊
}
class MyDerivedClass2 extends MyClass {
static get [Symbol.species]() {
return MyClass;
}
}
let instance1 = new MyDerivedClass1("foo"),
clone1 = instance1.clone(),
instance2 = new MyDerivedClass2("bar"),
clone2 = instance2.clone();
console.log(clone1 instanceof MyClass); // true
console.log(clone1 instanceof MyDerivedClass1); // true
console.log(clone2 instanceof MyClass); // true
console.log(clone2 instanceof MyDerivedClass2); // false
此處, MyDerivedClass1
繼承了 MyClass
,並且未修改 Symbol.species
屬性。由於
this.constructor[Symbol.species]
會返回 MyDerivedClass1
,當 clone()
被調用時,它就
返回了 MyDerivedClass1
的一個實例。 MyDerivedClass2
類也繼承了 MyClass
,但重寫了
Symbol.species
,讓其返回 MyClass
。當 clone()
在 MyDerivedClass2
的一個實例上被調
用時,返回值就變成 MyClass
的一個實例。使用 Symbol.species
,任意派生類在調用應當
返回實例的方法時,都可以判斷出需要返回什麼類型的值。
在類構造器中使用new.target
使用new.target
屬性能夠判斷當前實例對象是由哪個類構造器進行創建的,簡單的情況下,new.target
屬性就等於該類的構造器函數,同時new.target屬性也只能在構造器內被定義。
class Rec{
constructor(){
console.log(new.target===Rec);
}
}
class Square extends Rec{
}
let rec = new Rec();
let square = new Square();
輸出:true false
當創建Rec
對象實例時,new.target
指代的是Rec
自身的構造器,因此new.target===Rec
會返回true
,而Rec
的派生類Square
的new.target
會指向它自身的構造器,因此new.target===Rec
會返回false;
可以使用new.target來創建一個抽象基類:
class Rec{
constructor(){
if(new.target===Rec){
throw new Error('abstract class');
}
}
}
class Square extends Rec{
}
let rec = new Rec(); //Uncaught Error: abstract class
let square = new Square(); //不會報錯
當試圖創建一個Rec實例對象時,會拋出錯誤,因此Rec可以當做一個抽象基類。
5. 總結
-
ES6中的類使用關鍵字class進行定義,即可以採用類聲明的方式也可以採用類表達式進行定義。 此外,類構造器被調用時不能缺少 new ,確保了不能意外地將類作爲函數來調用用。
-
基於類的繼承允許你從另一個類、函數或表達式上派生新的類。這種能力意味着你可以調用一個函數來判斷需要繼承的正確基類,也允許你使用混入或其他不同的組合模式來創建一個新類。新的繼承方式讓繼承內置對象(例如數組) 也變爲可能,並且其工作符合預期。
-
可以使用
new.target
來判斷創建實例對象時所用的類構造器。利用new.target
可以用來創建一個抽象基類;
總之,類是 JS 的一項新特性,它提供了更簡潔的語法與更好的功能,通過安全一致的方式來自定義一個對象類型。