天天看點

ES6新特性實作面向對象程式設計,上萬字詳解用class文法定義類一、構造函數二、class的文法三、class的繼承四、class類的補充五、結束語

首先,寫這篇文章是因為我答應了一個粉絲要寫一篇ES6的

class

相關知識的要求,哈哈我是不是特别寵粉呀~其實同時也是幫助我自己複習一下知識點啦

ES6新特性實作面向對象程式設計,上萬字詳解用class文法定義類一、構造函數二、class的文法三、class的繼承四、class類的補充五、結束語

ES6中出現

class

文法,隻是建立構造函數的一種文法糖,那為何要新增一種文法去實作同一個功能呢?其實目的還是為了跟上一些主流程式設計語言的腳步,例如

java

C++

Python

,他們内部都是用

class

文法來實作的面向對象程式設計,是以咱們的

JavaScript

也不能落後,不然很多學習過

java c++ python

的小夥伴跑來學習

js

時,就很難了解構造函數這一概念了。

不相信的話,你們可以看看評論區,如果有學習過其它面向對象程式設計語言的,後來有學習過

JavaScript

的小夥伴可以在評論區分享一下自己對于構造函數這一概念的感想。

注意: 因為

class

文法涉及到大量的JavaScript中對象的概念,是以如果還沒有了解過對象的小夥伴可以檢視我之前寫的一篇剖析對象概念的文章,下面放上文章連結,點選即可跳轉

  • 充分了解JavaScript中的對象,順便弄懂你一直不明白的原型和原型鍊

好了話不多說,我們開始講解

class

  • 公衆号:前端印象
  • 不定時有送書活動,記得關注~
  • 關注後回複對應文字領取:【面試題】、【前端必看電子書】、【資料結構與算法完整代碼】、【前端技術交流群】

ES6——class文法

  • 一、構造函數
  • 二、class的文法
    • (1)體驗class文法
    • (2)constructor
    • (3)類方法的定義
    • (4)get函數和set函數
    • (5) 靜态方法
    • (6)執行個體屬性的簡易寫法
    • (7)靜态屬性
  • 三、class的繼承
    • (1)繼承的概念
    • (2)ES5中實作繼承
    • (3)ES6中class實作繼承
    • (4)super
    • (5)prototype和__proto__
  • 四、class類的補充
    • (1)不存在變量提升
    • (2)new.target
  • 五、結束語

一、構造函數

在學習

class

之前,我們先來回顧在ES6之前,建立一個執行個體對象是通過構造函數來實作的

//定義構造函數 Person
function Person(name, age) {
	this.name = name
	this.age = age
}

//在構造函數原型上定義方法 show
Person.prototype.show = function() {
	console.log('姓名:' + this.name)
	console.log('年齡:' + this.age)
}

//建立了一個Person類的執行個體
var person = new Person('Jack', 18)

console.log(person.name)              // Jack
console.log(person.age)               // 18
person.show()                         /* 姓名:Jack
									     年齡:18      */           

複制

我們通過

new

關鍵字調用構造函數,即可生成一個執行個體對象。不妨我們再來回顧一下

new

關鍵字的作用過程,即

var person = new Person('Jack', 18)

等價于以下代碼

var person = function (name='Jack', age = 18) {

	// 1.建立一個新的空對象指派給this
	var this = {}
	
	// 2.執行構造函數裡的所有代碼
	this.name = name
	this.age = age
	
	// 3.傳回this
	return this
}()           

複制

通過以上代碼我們可以得知,構造函數中的

this

指向的是新生成的執行個體對象,下文會講到,在

class

文法中,

this

在不同情況下會有不同的含義

二、class的文法

(1)體驗class文法

接下來,我們來看看

class

文法引入以後,建立執行個體對象有何變化,這裡我們就直接改寫上述例子了,友善大家進行比較

//用class定義一個類
class Person {
	constructor(name, age) {
		this.name = name
		this.age = age
	}
	show() {
		console.log('姓名:' + this.name)
		console.log('年齡:' + this.age)
	}
}

//生成Person類的一個執行個體對象person
var person = new Person('Jack', 18)

console.log(person.name)              // Jack
console.log(person.age)               // 18
person.show()                         /* 姓名:Jack
									     年齡:18      */           

複制

通過調用執行個體對象的屬性

name

age

以及方法

show

,我們可以看到,跟構造函數沒有任何的差別,是以說

class

文法就是構造函數的一個文法糖,即構造函數的另一種寫法,這兩者并無本質差別

其實我們還可以通過

typeof

來驗證一下

class

定義的類的類型

class Person {
	
}
console.log(typeof Person)           // function           

複制

(2)constructor

當我們用

class

定義了一個類,然後用關鍵字

new

調用該類,則會自動調用該類中的

constructor

函數,最後生成一個執行個體對象。

constructor

函數内部的

this

指向的也是新生成的執行個體對象。

如果要生成一個不需要任何屬性的執行個體對象,則我們不需要在

constructor

函數裡寫任何代碼,此時可以省略它,例如

class Person {
	//不寫constructor函數
	say() {
		console.log('hello world')
	}
}           

複制

上述代碼省略了

constructor

函數,此時JavaScript會預設生成一個空的

constructor

函數,例如

class Person {
	constructor() {
	
	}
	say() {
		console.log('hello world')
	}
}           

複制

以上兩段代碼是等價的

也正是因為

constructor

函數的存在,

class

定義的類必須通過

new

來建立執行個體對象,否則就會報錯

class Person {

}
var person = Person()

/*
報錯
var person = Person()
             ^
TypeError: Class constructor Person cannot be invoked without 'new'
*/           

複制

而傳統的構造函數就可以不通過

new

來調用,因為其本身就是一個函數,若不加關鍵字

new

,則相當于直接執行該函數

(3)類方法的定義

在傳統的構造函數中,為了使每個執行個體對象都擁有共同的方法,在構造函數的原型上進行方法的定義,例如

function Person() {}
Person.prototype.show = function () {
	console.log('hello world')
}           

複制

是以,

class

文法定義的方法也是在原型上的,不過這裡稱之為類的原型上,同時省略了大量的代碼,直接将方法寫在

class

内即可

class Person {
	//在Person類的原型上定義了方法 show
	show() {
		console.log('hello world')
	}
	//在Person類的原型上定義了方法 hide
	hide() {
		console.log('bye world')
	}
}           

複制

細心的小夥伴肯定發現了,雖然方法都是寫在

{}

内的,但是每個方法之間無需用

,

隔開,否則就會報錯,這個一定要注意一下

其實以上定義類方法的代碼等價于以下代碼

class Person {}

//在Person類的原型上定義了方法 show
Person.prototype.show = function () {
	console.log('hello world')
}

//在Person類的原型上定義了方法 hide
Person.prototype.hide = function () {
	console.log('bye world')
}           

複制

這其實跟為構造函數定義方法一樣,但是整體看上去代碼量就非常得大

雖說構造函和類兩者定義的方法都是定義在其原型上的,但還是有略微的差別,即前者定義的方法具有 可枚舉性;而後者定義的方法具有 不可枚舉性。

為了驗證兩者差別,我們要用到ES5中提供的兩個新方法

  • Object.keys(): 會傳回一個數組,數組中的元素就是對象中可枚舉的自有屬性名
  • Object.getOwnPropertyNames(): 傳回一個數組,數組中的元素是對象中所有自有屬性的名稱,不管屬性是否具有可枚舉性都能被傳回。

首先我們來驗證一下構造函數定義的方法的枚舉性

function Person() {}

Person.prototype.show = function () {
	console.log('hello world')
}
Person.prototype.hide = function() {
	console.log('bye world')
}

Object.keys(Person.prototype)   // [ 'show', 'hide' ]
Object.getOwnPropertyNames(Person.prototype)   // [ 'constructor', 'show', 'hide' ]           

複制

我們可以看到,

Object.keys()

方法傳回

[ 'show', 'hide' ]

,證明這定義的兩個方法是自有屬性且是可枚舉的;

Object.getOwnPropertyNames()

方法傳回

[ 'constructor', 'show', 'hide' ]

,說明構造函數内有一個自有屬性方法

constructor

,且不可枚舉。

接下來我們再來看一下

class

定義的類中定義的方法的枚舉性

class Person {
	show() {
		console.log('hello world')
	}
	hide() {
		console.log('bye world')
	}
}

Object.keys(Person.prototype)   // []
Object.getOwnPropertyNames(Person.prototype)   // [ 'constructor', 'show', 'hide' ]           

複制

我們看到

Object.keys()

傳回

[]

,說明

class

類定義的方法具有不可枚舉性;

Object.getOwnPropertyNames()

方法傳回

[ 'constructor', 'show', 'hide' ]

,可以看到同樣也具有一個不可枚舉的自有屬性

constructor

方法。

(4)get函數和set函數

class

類中,可以使用兩個内部定義的函數,即

get

set

,文法為

get/set 屬性名() {}

,分别表示讀取屬性/設定屬性時調用此函數,其中

set

函數接收一個參數,表示所設定的值

我們來看個例子

class Person {
	get number() {
		return 18
	}
	set number(value) {
		console.log('現在的number值為:' + value)
	}
}

var person = new Person()

//通路屬性number
person.number  //   18

//設定屬性number為20
person.number = 20  // 列印:現在的number值為:20           

複制

當我們通路屬性

number

時,會調用

get number() {}

函數,故傳回

18

;當設定屬性

number

的值為

20

時,會調用

set number() {}

函數,故列印了

現在的number值為:20

表面上看,

get

set

函數是方法,但其實并不是,我們可以用

Object.getOwnPropertyNames()

方法來驗證一下

Object.getOwnPropertyNames(Person.prototype)

// [ 'constructor', 'number' ]           

複制

我們可以看到,傳回的數組中隻有

class

類自帶的

constructor

函數和

number

屬性,并沒有看到

get

set

函數。

了解ES5中對象概念的小夥伴應該知道,對象中有兩個存儲器屬性,分别為

getter

setter

,它們是對象中某個屬性的特性,并且可以通過

Object.getOwnPropertyDescriptor()

方法獲得對象中某個屬性的屬性描述符

//查詢Person.prototype中屬性number的屬性描述符
Object.getOwnPropertyDescriptor(Person.prototype, 'number')

/*
{
  get: [Function: get number],
  set: [Function: set number],
  enumerable: false,
  configurable: true
}
*/           

複制

是以,我們在

class

類中寫的

get

set

函數隻是設定了某個屬性的屬性特性,而不是該類的方法。

(5) 靜态方法

class

類中的方法都是寫在原型上的,是以生成的執行個體對象可以直接調用。現在有一個關鍵字

static

,若寫在方法的前面,則表示此方法不會被寫在原型上,而隻作為該類的一個方法,這樣的方法叫做靜态方法;相反,若沒加關鍵字

static

的方法就叫做非靜态方法

我們來看一下具體的例子

class Person {
	show() {
		console.log('我是非靜态方法show')
	}
	static show() {
		console.log('我是靜态方法show')
	}
	static hide() {
		console.log('我是靜态方法hide')
	}
}

Person.show()    // 我是靜态方法show

var person = new Person()

person.show()    // 我是非靜态方法show

person.hide()    /*	person.hide()
                           ^
			  TypeError: person.hide is not a function
				
				*/           

複制

我們分析一下這個例子:

首先我們直接調用

Person

類的

show

方法,實際調用的就是有關鍵字

static

show

方法;

然後我們生成了一個執行個體對象

person

,然後調用

person

執行個體對象上的

show

方法,實際調用的就是沒有關鍵字

static

show

方法,從這我們可以看出,靜态方法和非靜态方法可以重名;

最後我們調用了

person

執行個體對象上的

hide

方法,但報錯了,因為在

class

類中,我們定義的是靜态方法,即有關鍵字

static

hide

方法,也就是此方法沒有被寫進類的原型中,因而執行個體對象

person

無法調用此方法。

我們都知道,類中定義的方法内的

this

指向的是執行個體對象,但在靜态方法中的

this

指向的是類對象

我們來看一個例子

class Person {
	constructor() {
		this.name = 'Lpyexplore'
	}
	show() {
		console.log(this.name)
	}
	static cite() {
		this.show()
	}
	static show() {
		console.log('我是非靜态方法show')
	}
}

Person.cite()     // 我是非靜态方法show

var person = new Person()

person.show()     // Lpyexplore           

複制

我們來分析一下這段代碼:

首先我們直接調用

Person

類的靜态方法

cite

,執行代碼

this.show()

,因為靜态方法中的

this

指向

Person

類,是以其實調用的就是靜态方法

show

,是以列印了

我是非靜态方法show

然後我們生成了一個執行個體對象

person

,調用

person

show

方法,因為在非靜态方法

show

中,

this

指向的是執行個體對象

person

,是以列印了

Lpyexplore

(6)執行個體屬性的簡易寫法

原先我們為執行個體對象定義的屬性都是寫在

constructor

函數中的,例如

class Person {
	constructor() {
		this.name = 'Lpyexplore'
		this.age = 18
	}
	show() {
		console.log('hello world')
	}
}

var person = new Person()

console.log(person.name)        // Lpyexplore
console.log(person.age)         // 18           

複制

現在我們用執行個體對象的屬性新寫法來改寫以上代碼

class Person {
	name = 'Lpyexplore'
	age = 18
	show() {
		console.log('hello world')
	}
}

var person = new Person()

console.log(person.name)        // Lpyexplore
console.log(person.age)         // 18           

複制

這種寫法就是将

constructor

函數中的屬性定義放到了外部,同時不需要寫

this

,因為此時的屬性定義與其他方法也處于同一個層級。是以這樣的寫法看上去就會比較一目了然,一眼就能看到執行個體對象有幾個屬性有幾個方法。

雖然這樣的寫法比較簡便,但也有一定的缺點,那就是用這種寫法定義的屬性是寫死的。

我們都知道在生成執行個體對象時,可以傳入參數,傳入的參數會作為

constructor

函數的參數,是以我們在

constructor

函數中定義的屬性的值就可以動态地根據參數改變而改變。

而執行個體屬性的簡易寫法就無法根據參數的改變而改變,是以用這種寫法的時候需要稍微注意一下。

(7)靜态屬性

既然有靜态方法,那怎麼能少了靜态屬性呢?其實,原本的

class

類中是沒有靜态屬性這個概念的,後來才加上的。靜态屬性就隻屬于

class

類的屬性,而不會被執行個體對象通路到的屬性。

同樣的,靜态屬性的申明就是在屬性的前面加關鍵字

static

。上面我們剛講到,執行個體對象的屬性的定義可以不寫在

constructor

函數中,而是直接寫在外部,此時我們可以暫且稱之為非靜态屬性

class Person {
	name = '我是執行個體對象的name屬性'
	static name = '我是Person類的name屬性'
	static age = 18
}

console.log(Person.name)   // 我是Person類的name屬性

var person = new Person()

console.log(person.name)   // 我是執行個體對象的name屬性

console.log(person.age)    // undefined           

複制

這段代碼中,定義了非靜态屬性

name

、靜态屬性

name

和 靜态屬性

age

是以我們在通路

Person

類的

name

屬性時,通路的是靜态屬性

name

,即加了關鍵字

static

name

屬性;

生成執行個體對象

person

,通路其

name

屬性,實際通路的就是非靜态屬性

name

,即沒有加關鍵字

static

name

屬性;

最後我們通路執行個體對象

person

age

屬性,傳回了

undefined

。因為

age

是靜态屬性,是屬于

Person

類的,而不會被執行個體對象

person

通路到。

三、class的繼承

繼承是面向對象程式設計中一個非常重要的概念,那什麼是繼承呢?

(1)繼承的概念

繼承就是使一個類獲得另一個類的屬性和方法。就好比一個手藝精湛的師傅傳授給你他所有的畢生絕學,那麼就相當于你繼承了他,此時你既學會了你師傅教你的技能,同時你也一定有屬于自己的技能,這不是從你師傅那學來的。

(2)ES5中實作繼承

其實在ES5中是通過修改原型鍊實作繼承的,我們可以來看一下簡單的例子

// 建立構造函數 Parent
function Parent() {
	// 定義了執行個體對象屬性 name1
	this.name1 = 'parent'
}

// 為 Parent原型定義方法 show1
Parent.prototype.show1 = function() {
	console.log('我是Parent的show1方法')
}

// 建立構造函數 Child
function Child() {
	this.name2 = 'child'
}

// 将構造函數 Child的原型設定成 Parent的執行個體對象
Child.prototype = new Parent()

// 為Child原型定義方法 show2
Child.prototype.show2 = function() {
	console.log('我是Child的show2方法')
}

// 生成執行個體對象 child
var child = new Child()

console.log(child.name1)          // parent
console.log(child.name2)          // child
child.show1()                     // 我是Parent的show1方法
child.show2()                     // 我是Child的show2方法           

複制

我們可以看到,我們通過改變構造函數

Child

的原型

prototype

為構造函數

Parent

生成的執行個體對象,實作了繼承,即通過構造函數

Child

生成的執行個體對象具有

Parent

中定義的屬性

name1

和方法

show1

,同時也具有屬于自己的屬性

name2

和方法

show2

(3)ES6中class實作繼承

ES5中實作繼承的寫法顯然有些麻煩,是以在

class

類中,我們可以通過關鍵字

extends

來實作繼承

我們來改寫一下ES5中的繼承實作

class Parent{
    constructor() {
        this.name1 = 'parent'
    }
    show1() {
        console.log('我是Parent的show1方法')
    }
}

// Child類 繼承 Parent類
class Child extends Parent{
    constructor() {
        super();
        this.name2 = 'child'
    }
    show2() {
        console.log('我是Child的show2方法')
    }
}

var child = new Child()

console.log(child.name1)          // parent
console.log(child.name2)          // child
child.show1()                     // 我是Parent的show1方法
child.show2()                     // 我是Child的show2方法           

複制

繼承得實作整體上看上去非常得簡潔

在上述代碼中,我們看到了,我們在定義

Child

類時用到了關鍵字

extends

,申明了

Child

類繼承

Parent

類,同時在

Child

類得

constructor

函數中調用了

super

函數。僅僅用兩個關鍵字就實作了繼承,這裡我們要對

super

進行詳細得講解

(4)super

在ES6中規定了,在子類繼承了父類以後,必須先在子類的

constructor

函數中調用

super

函數,其表示的就是父級的

constructor

函數,作用就是為子類生成

this

對象,将父類的屬性和方法指派到子類的

this

上。是以,若沒有調用

super

函數,則子類無法擷取到

this

對象,緊接着就會報錯

class A{
    constructor() {
        this.name1 = 'A'
    }
}

class B extends A{
    constructor() {
        this.name2 = 'B'
    }
}

var b = new B()

/*
        this.name2 = 'B'
        ^
ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor

*/           

複制

上述代碼中,

B

類繼承

A

類,但

B

類的

constructor

函數中沒有調用

super

函數,是以沒有生成

this

對象,是以

this.name2 = 'B'

就報錯了

若子類省略了

constructor

函數,則預設會幫你調用

super

函數的

class A{
    constructor() {
        this.name1 = 'A'
    }
}

class B extends A{

}

var b = new B()

// 沒有報錯           

複制

super()

代表的是父類的構造函數,其實

super

還可以作為對象使用,即不作為函數調用。當

super

在子類的普通方法内時,指向的是父類的原型對象;在子類的靜态方法内時,指向的時父類

class A{
	show1() {
		console.log('我是A類的show1方法')
	}
}

class B extends A{
	constructor() {
		super()
	}
	show2() {
		super.show1()
	}	
}

var b = new B()

b.show2()              // 我是A類的show1方法           

複制

上述代碼,

B

類繼承

A

類,其中

A

類有一個

show1

方法,是寫在其原型

A.prototype

上的,而在

B

類的

show2

方法中調用了

super.show1()

,我們說過

super

在普通的方法中指向的是父類的原型對象,是以

super.show1()

相當于

A.prototype.show1()

我們再來看一個

super

在子類的靜态方法中的例子

class A{
    static hide1() {
        console.log('我是A類的hide1方法')
    }
}

class B extends A{
    constructor() {
        super()
    }
    static hide2() {
        super.hide1()
    }
}

B.hide2()                     // 我是A類的hide1方法           

複制

上述代碼,

B

類繼承

A

類,

B

類在其靜态方法

hide2

中調用了

super.hide1()

,因為

super

在靜态方法中指向的是父類,是以

super.hide1()

就相當于

A.hide1()

說到靜态方法,其實類的繼承,也是可以繼承靜态方法的

class A{
	static show() {
		console.log('我是A類的show方法')
	}
}

class B extends A{
	
}

B.show()                    // 我是A類的show方法           

複制

還需要注意的是,當我們在子類的普通方法中通過

super

調用父類的方法時,方法中的

this

指向的是目前子類的執行個體對象

class A {
    constructor() {
        this.name = 'Jack'
    }
    show1() {
        console.log(this.name)
    }
}

class B extends A{
    constructor() {
        super();
        this.name = 'Lpyexplore'
    }
    show2() {
        super.show1()
    }
}

var b = new B()

b.show2()                 // Lpyexplore           

複制

那麼,當我們在子類的靜态方法中通過

super

調用父類的方法時,方法中的

this

指向的是子類,而不是子類的執行個體對象

class A {
    constructor() {
        this.x = 1
    }
    static show1() {
        console.log(this.x)
    }
}

class B extends A{
    constructor() {
        super();
        this.x = 2
    }
    static show2() {
        super.show1()
    }
}

B.show2()                 // undefined

B.x = 3
B.show2()                 // 3           

複制

上述代碼中,我們在

B

類的靜态方法

show2

中通過

super

調用了

A

類的靜态方法

show1

,執行代碼

console.log(this.x)

,此時的

this

指向的是

B

類,但因為

B

類的

constructor

函數中定義的屬性

x

是定義在

B

類的執行個體對象上的,是以

this.x

傳回的是

undefined

是以我們在

B

類上定義一個屬性

x

并且值為

3

,此時再此調用

B.show2()

,傳回的就是

3

了。

(5)prototype和__proto__

class

類中有兩個屬性,分别表示着一條繼承鍊,即

prototype

__proto__

子類的

__proto__

總是指向父類;

子類的

prototype

__proto__

總是指向父類的原型

我們來驗證一下

class A{}

class B extends A{}

console.log(B.__proto__ === A)                     // true
console.log(B.prototype.__proto__ === A.prototype) // true           

複制

四、class類的補充

對于

class

類還有幾點需要補充以下

(1)不存在變量提升

構造函數本身就是個函數,存在變量提升,是以通過構造函數生成執行個體對象時,可以将構造函數寫在生成執行個體對象的代碼後面

var person = new Person()

function Person() {
	this.name = 'Lpyexplore'
}

// 沒有報錯           

複制

雖然

class

類的資料類型也屬于

function

,但是它是不存在變量提升的,即不可以在申明類之前生成執行個體對象,否則就會報錯

var person = new Person()

class Person{}

/*
報錯:
var person = new Person()
             ^
ReferenceError: Cannot access 'Person' before initialization

*/           

複制

(2)new.target

class

類必須通過

new

來生成執行個體對象,是以ES6引入了一個新的屬性

new.target

,該屬性一般用于

constructor

函數中,表示通過關鍵字

new

作用的構造函數的名稱,若不是通過

new

指令調用的,則傳回

undefined

class A{
	constructor() {
		if(new.target === 'undefined') {
			console.log('請通過關鍵字new調用')
		} else {
			console.log(new.target)
		}
	}
}


var a = new A()              // [class A]           

複制

當子類繼承父類,并調用父類的

constructor

函數時,

new.target

傳回的是子類的構造函數名以及繼承自哪個父類

class A{
	constructor() {
		console.log(new.target)
	}
}

class B extends A{
	constructor() {
		super()
	}
}

var b = new B()               // [class B extends A]           

複制

五、結束語

好了,ES6的

class

文法就講到這裡,希望這篇文章能幫助到大家。

創作不易,喜歡的加個關注,點個收藏,給個贊~ 帶你們在Python爬蟲的過程中學習Web前端