ES6學習筆記(九)【class】

風靈使發表於2019-04-06

主要知識點:類宣告、類表示式、類的重要要點以及類繼承

在這裡插入圖片描述

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關鍵字來定義一個型別,有這樣幾個要點:

  1. 類宣告不會被提升,這與ES6之前通過函式定義不同。類宣告與使用let定義一樣,因此也存在暫時性死區;
  2. 類宣告中的所有程式碼會自動執行在嚴格模式下,並且無法退出嚴格模式;
  3. 類的所有方法都是不可列舉的;
  4. 類的所有內部方法都沒有[[Constructor]],因此使用new來呼叫他們會丟擲錯誤;
  5. 呼叫類構造器時不使用new,會丟擲錯誤;
  6. 試圖在類的內部方法中重寫類名,會丟擲錯誤;

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

關於類繼承,還有這樣幾個要點:

  1. **在派生類中方法會覆蓋掉基類中的同名方法,**例如在派生類Square中有getArea()方法的話就會覆蓋掉基類Rec中的getArea()方法;
  2. 如果基類中包含了靜態成員,那麼這些靜態成員在派生類中也是可以使用的。注意:靜態成員只能通過類名進行訪問,而不是使用物件例項進行訪問

從表示式中派生類

在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的派生類Squarenew.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. 總結

  1. ES6中的類使用關鍵字class進行定義,即可以採用類宣告的方式也可以採用類表示式進行定義。 此外,類構造器被呼叫時不能缺少 new ,確保了不能意外地將類作為函式來呼叫用。

  2. 基於類的繼承允許你從另一個類、函式或表示式上派生新的類。這種能力意味著你可以呼叫一個函式來判斷需要繼承的正確基類,也允許你使用混入或其他不同的組合模式來建立一個新類。新的繼承方式讓繼承內建物件(例如陣列) 也變為可能,並且其工作符合預期。

  3. 可以使用new.target來判斷建立例項物件時所用的類構造器。利用new.target可以用來建立一個抽象基類;

總之,類是 JS 的一項新特性,它提供了更簡潔的語法與更好的功能,通過安全一致的方式來自定義一個物件型別。

相關文章