文章目錄
Class 的繼承
1.0 簡介
Class 可以通過
extends
關鍵字實現繼承, 這邊 ES5通過原型鏈實現繼承, 要清晰和方便很多class Point { } class ColorPoint extends Point { }
上面代碼定義了一個
ColorPoint
類,該類通過extends
關鍵字,繼承了Point
類的所有屬性和方法。但是由於沒有部署任何代碼,所以這兩個類完全一樣,等於複製了一個Point
類。下面,我們在ColorPoint
內部加上代碼。class ColorPoint extends Point { constructor(x, y, color) { super(x, y); // 調用父類的constructor(x, y) this.color = color; } toString() { return this.color + ' ' + super.toString(); // 調用父類的toString() } }
子類必須在
constructor
方法中調用super
方法,否則新建實例時會報錯。這是因爲子類自己的this
對象,必須先通過父類的構造函數完成塑造,得到與父類同樣的實例屬性和方法,然後再對其進行加工,加上子類自己的實例屬性和方法。如果不調用super
方法,子類就得不到this
對象。class Point { /* ... */ } class ColorPoint extends Point { constructor() { } } let cp = new ColorPoint(); // ReferenceError
上面代碼中,
ColorPoint
繼承了父類Point
,但是它的構造函數沒有調用super
方法,導致新建實例時報錯。
ES5 的繼承,實質是先創造子類的實例對象
this
,然後再將父類的方法添加到this
上面(Parent.apply(this)
)。ES6 的繼承機制完全不同,實質是先將父類實例對象的屬性和方法,加到
this
上面(所以必須先調用super
方法),然後再用子類的構造函數修改this
。
如果子類沒有定義
constructor
方法,這個方法會被默認添加,代碼如下。也就是說,不管有沒有顯式定義,任何一個子類都有constructor
方法。class ColorPoint extends Point { } // 等同於 class ColorPoint extends Point { constructor(...args) { super(...args); } }
另一個需要注意的地方是,在子類的構造函數中,只有調用
super
之後,纔可以使用this
關鍵字,否則會報錯。這是因爲子類實例的構建,基於父類實例,只有super
方法才能調用父類實例。也就是說, super() 方法必須在 constructor 方法體內的最上面調用
class Point { constructor(x, y) { this.x = x; this.y = y; } } class ColorPoint extends Point { constructor(x, y, color) { this.color = color; // ReferenceError super(x, y); this.color = color; // 正確 } }
父類的靜態方法, 也會被子類繼承
父類的類上的普通方法的繼承
看以下案例:
class Animal { eat = this.eat // 父類的類上的方法(也就是父類的原型上的方法)不會被繼承, 除非添加到實例 this上 constructor(name, age, sex) { this.name = name this.age = age this.sex = sex } sayHello() { // 父類的類上的普通方法不能被繼承 console.log('父類的類上的普通方法sayHello...'); } eat(){ console.log('父類類上的普通方法, 顯示添加到實例原型this 上 eat...'); } static staticFn(){ console.log('父類的靜態方法,可以被繼承,但是隻能被子類調用, 不能被子類實例調用 staticFn...'); } } class Dog extends Animal { constructor(name, age, sex, color) { super(name, age, sex) this.color = color } sayHi() { console.log('-------- Dog類', this) } } const dog = new Dog('d', 12, 'b', 'yellow') dog.sayHi() dog.eat() // 父類的方法添加到 this 實例上之後可以被繼承 Dog.staticFn() // 父類的靜態方法可以被繼承, 但是隻能子類的類調用 dog.sayHello() // 父類的類上普通方法能被繼承, 但是不在子類 this實例上
2.0 Object.getPrototypeOf()
Object.getPrototypeOf
方法可以用來從子類獲取父類class Animal { constructor(name, age, sex) { this.name = name } sayHello() { // 父類的類上的普通方法不能被繼承 console.log('父類的類上的普通方法sayHello...'); } } class Dog extends Animal { constructor(name, age, sex, color) { super(name, age, sex) this.color = color } } # console.log(Object.getPrototypeOf(Dog) === Animal); // true
因此,可以使用這個方法判斷,一個類是否繼承了另一個類。
3. super 關鍵字
super
這個關鍵字, 即可以當做函數使用, 也可以當做對象使用. 在這兩種情況下, 它的用法完全不同.
第一種情況: ==
super
作爲函數調用時, 代表父類的構造函數==ES6要求, 子類的構造函數必須執行一次super
函數class A {} class B extends A { constructor() { super(); } }
上面代碼中,子類
B
的構造函數之中的super()
,代表調用父類的構造函數。這是必須的,否則 JavaScript 引擎會報錯。
注意,super
雖然代表了父類A
的構造函數,但是返回的是子類B
的實例,即super
內部的this
指的是B
的實例,因此super()
在這裏相當於``A.prototype.constructor.call(this)
。
super()
內部的this
指向的是B
。作爲函數時,
super
只能用在子類的構造函數中, 用在其他地方會報錯.class A {} class B extends A { m() { super(); // 報錯 } }
上面代碼中,
super()
用在B
類的m
方法之中,就會造成語法錯誤。
第二種情況:
super
作爲對象時.
- 在普通方法中, 指向父類的原型對象.
- 在靜態方法中, 指向父類.
class Animal { constructor() { } sayHiParent(){ console.log('我是父類的普通方法... ...'); } } class Dog extends Animal { constructor() { super() } sayHi(){ super.sayHiParent() // 我是父類的普通方法 Animal.prototype.sayHiParent() // // 我是父類的普通方法 } } const dog = new Dog() dog.sayHi()
上面代碼中, 子類
Dog
當中的super.sayHiParent()
, 就是將super
當做一個對象使用, 此時,super
在普通方法之中, 指向A.prototype,
, 所以super.sayHiParent()
就相當於Animal.prototype.sayHiParent()
這裏需要注意: 由於
super
指向父類的原型對象, 所以定義在父類實例上的方法或屬性, 是無法通過super
調用的.class Animal { constructor() { this.name = 'super實例上的屬性' } } class Dog extends Animal { sayHi(){ return super.name } } const dog = new Dog() console.log(dog.sayHi()); // undefined 所以定義在父類實例上的屬性, 無法通過super調用
在上面代碼中
name
是父類Animal
實例的屬性,super.name
就引用不到它.
如果屬性定義在父類的原型對象上,
super
就可以取到class Animal { } Animal.prototype.name = 'super實例上的屬性' class Dog extends Animal { sayHi() { return super.name } } const dog = new Dog() console.log(dog.sayHi()); // super實例上的屬性
上面代碼中, 屬性
name
是定義在Animal.prototype
上面上的, 所以super.name
可以取到它的值.
ES6 規定, 在子類普通方法中, 通過
super
調用父類的方法時, 方法內部的this
指向當前子類的實例.class A { constructor() { this.x = 1; } print() { console.log(this.x); } } class B extends A { constructor() { super(); this.x = 2; } m() { super.print(); } } let b = new B(); b.m() // 2
上面代碼中,
super.print()
雖然調用的是A.prototype.print()
,但是A.prototype.print()
內部的this
指向子類B
的實例,導致輸出的是2
,而不是1
。也就是說,實際上執行的是super.print.call(this)
。
由於
this
指向子類實例,所以如果通過super
對某個屬性賦值,這時super
就是this
,賦值的屬性會變成子類實例的屬性。class A { constructor() { this.x = 1; } } class B extends A { constructor() { super(); this.x = 2; super.x = 3; console.log(super.x); // undefined console.log(this.x); // 3 } } let b = new B();
上面代碼中,
super.x
賦值爲3
,這時等同於對this.x
賦值爲3
。而當讀取super.x
的時候,讀的是A.prototype.x
,所以返回undefined
。
如果
super
作爲對象, 用在靜態方法之中, 這時super
將指向父類 , 而不是父類的原型對象.class Parent { static myMethod(msg) { console.log('static', msg); } myMethod(msg) { console.log('instance', msg); } } class Child extends Parent { static myMethod(msg) { super.myMethod(msg); // super在靜態方法中指向父類, 而不是父類的原型 } myMethod(msg) { super.myMethod(msg); // super在普通方法中指向父類的原型 } } Child.myMethod(1); // static 1 var child = new Child(); child.myMethod(2); // instance 2
上面代碼中,
super
在靜態方法之中指向父類,在普通方法之中指向父類的原型對象。
另外,**在子類的靜態方法中通過
super
調用父類的方法時,方法內部的this
指向當前的子類,**而不是子類的實例。class A { constructor() { this.x = 1; } static print() { console.log(this.x); } } class B extends A { constructor() { super(); this.x = 2; } static m() { super.print(); } } B.x = 3; B.m() // 3
上面代碼中,靜態方法
B.m
裏面,super.print
指向父類的靜態方法。這個方法裏面的this
指向的是B
,而不是B
的實例
注意:
使用
super
的時候,必須顯式指定是作爲函數、還是作爲對象使用,否則會報錯。class A {} class B extends A { constructor() { super(); console.log(super); // 報錯 } }
上面代碼中,
console.log(super)
當中的super
,無法看出是作爲函數使用,還是作爲對象使用,所以 JavaScript 引擎解析代碼的時候就會報錯。這時,如果能清晰地表明super
的數據類型,就不會報錯。class A {} class B extends A { constructor() { super(); console.log(super.valueOf() instanceof B); // true } } let b = new B();
上面代碼中,
super.valueOf()
表明super
是一個對象,因此就不會報錯。同時,由於super
使得this
指向B
的實例,所以super.valueOf()
返回的是一個B
的實例。
4.0 類的 prototype屬性和__proto__屬性
大多數瀏覽器的 ES5 實現之中, 每一個對象都有
__proto__屬性, 指向對一個的構造函數的
prototype屬性, Class 作爲構造函數的語法糖, 同時有
prototype屬性和
proto 屬性, 因此同時存在兩條繼承鏈
- 子類的
__proto__
屬性, 表示構造函數的繼承, 總是指向父類.- 子類的
prototype
屬性的__proto__
屬性, 表示方法的繼承, 總是指向父類的prototype
屬性.class A { } class B extends A { } B.__proto__ === A // true B.prototype.__proto__ === A.prototype // true
上面代碼中,子類
B
的__proto__
屬性指向父類A
,子類B
的prototype
屬性的__proto__
屬性指向父類A
的prototype
屬性。這樣的結果是因爲,類的繼承是按照下面的模式實現的。
class A { } class B { } // B 的實例繼承 A 的實例 Object.setPrototypeOf(B.prototype, A.prototype); // B 繼承 A 的靜態屬性 Object.setPrototypeOf(B, A); const b = new B();
這兩條繼承鏈,可以這樣理解:
- 作爲一個對象,子類(
B
)的原型(__proto__
屬性)是父類(A
);- 作爲一個構造函數,子類(
B
)的原型對象(prototype
屬性)是父類的原型對象(prototype
屬性)的實例。
4.1 實例的 __proto__
屬性
子類實例的
__proto__
屬性的__proto__
屬性, 指向父類實例的__proto__
屬性, 也就是說, 子類的原型的原型, 就是父類的原型.var p1 = new Point(2, 3); var p2 = new ColorPoint(2, 3, 'red'); p2.__proto__ === p1.__proto__ // false p2.__proto__.__proto__ === p1.__proto__ // true
總結:
上一章: ES6類Class 的基本語法, 靜態方法, 實例屬性新寫法
下一章:
交流學習添加微信(備註技術交流學習):
Gene199302
該博客爲學習阮一峯 ES6入門課所做的筆記記錄, 僅用來留作筆記記錄和學習理解