首先,寫這篇文章是因為我答應了一個粉絲要寫一篇ES6的
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前端