“睡服”面试官系列第二十二篇之class的继承(建议收藏学习)
目錄
1. 簡介
2. Object.getPrototypeOf()
3. super 關鍵字
4. 類的 prototype 屬性和__proto__屬性
4.1extends 的繼承目標
4.2實例的 __proto__ 屬性
5. 原生構造函數(shù)的繼承
6. Mixin 模式的實現(xiàn)
1. 簡介
Class 可以通過 extends 關鍵字實現(xiàn)繼承,這比 ES5 的通過修改原型鏈實現(xiàn)繼承,要清晰和方便很多
class Point { } class ColorPoint extends Point { }上面代碼定義了一個 ColorPoint 類,該類通過 extends 關鍵字,繼承了 Point 類的所有屬性和方法。但是由于沒有部署任何代碼,所以這兩個類完全一
樣,等于復制了一個 Point 類。下面,我們在 ColorPoint 內(nèi)部加上代碼。
上面代碼中, constructor 方法和 toString 方法之中,都出現(xiàn)了 super 關鍵字,它在這里表示父類的構造函數(shù),用來新建父類的 this 對象。
子類必須在 constructor 方法中調用 super 方法,否則新建實例時會報錯。這是因為子類沒有自己的 this 對象,而是繼承父類的 this 對象,然后對其進
行加工。如果不調用 super 方法,子類就得不到 this 對象。
另一個需要注意的地方是,在子類的構造函數(shù)中,只有調用 super 之后,才可以使用 this 關鍵字,否則會報錯。這是因為子類實例的構建,是基于對父類
實例加工,只有 super 方法才能返回父類實例。
上面代碼中,子類的 constructor 方法沒有調用 super 之前,就使用 this 關鍵字,結果報錯,而放在 super 方法之后就是正確的。
下面是生成子類實例的代碼。
上面代碼中,實例對象 cp 同時是 ColorPoint 和 Point 兩個類的實例,這與 ES5 的行為完全一致。
最后,父類的靜態(tài)方法,也會被子類繼承。
上面代碼中, hello() 是 A 類的靜態(tài)方法, B 繼承 A ,也繼承了 A 的靜態(tài)方法。
2. Object.getPrototypeOf()
Object.getPrototypeOf(ColorPoint) === Point // tru因此,可以使用這個方法判斷,一個類是否繼承了另一個類。
3. super 關鍵字
super 這個關鍵字,既可以當作函數(shù)使用,也可以當作對象使用。在這兩種情況下,它的用法完全不同。
第一種情況, super 作為函數(shù)調用時,代表父類的構造函數(shù)。ES6 要求,子類的構造函數(shù)必須執(zhí)行一次 super 函數(shù)。
上面代碼中,子類 B 的構造函數(shù)之中的 super() ,代表調用父類的構造函數(shù)。這是必須的,否則 JavaScript 引擎會報錯。
注意, super 雖然代表了父類 A 的構造函數(shù),但是返回的是子類 B 的實例,即 super 內(nèi)部的 this 指的是 B ,因此 super() 在這里相當于
A.prototype.constructor.call(this) 。
上面代碼中, new.target 指向當前正在執(zhí)行的函數(shù)。可以看到,在 super() 執(zhí)行時,它指向的是子類 B 的構造函數(shù),而不是父類 A 的構造函數(shù)。也就是
說, super() 內(nèi)部的 this 指向的是 B 。
作為函數(shù)時, super() 只能用在子類的構造函數(shù)之中,用在其他地方就會報錯。
上面代碼中, super() 用在 B 類的 m 方法之中,就會造成句法錯誤。
第二種情況, super 作為對象時,在普通方法中,指向父類的原型對象;在靜態(tài)方法中,指向父類。
上面代碼中,子類 B 當中的 super.p() ,就是將 super 當作一個對象使用。這時, super 在普通方法之中,指向 A.prototype ,所以 super.p() 就相當于
A.prototype.p() 。
這里需要注意,由于 super 指向父類的原型對象,所以定義在父類實例上的方法或屬性,是無法通過 super 調用的
上面代碼中, p 是父類 A 實例的屬性, super.p 就引用不到它。
如果屬性定義在父類的原型對象上, super 就可以取到。
上面代碼中,屬性 x 是定義在 A.prototype 上面的,所以 super.x 可以取到它的值。
ES6 規(guī)定,通過 super 調用父類的方法時,方法內(nèi)部的 this 指向子類
上面代碼中, super.print() 雖然調用的是 A.prototype.print() ,但是 A.prototype.print() 內(nèi)部的 this 指向子類 B ,導致輸出的是 2 ,而不是 1 。
也就是說,實際上執(zhí)行的是 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 作為對象,用在靜態(tài)方法之中,這時 super 將指向父類,而不是父類的原型對象。
上面代碼中, super 在靜態(tài)方法之中指向父類,在普通方法之中指向父類的原型對象。
注意,使用 super 的時候,必須顯式指定是作為函數(shù)、還是作為對象使用,否則會報錯。
上面代碼中, console.log(super) 當中的 super ,無法看出是作為函數(shù)使用,還是作為對象使用,所以 JavaScript 引擎解析代碼的時候就會報錯。這
時,如果能清晰地表明 super 的數(shù)據(jù)類型,就不會報錯
上面代碼中, super.valueOf() 表明 super 是一個對象,因此就不會報錯。同時,由于 super 使得 this 指向 B ,所以 super.valueOf() 返回的是一個 B
的實例。
最后,由于對象總是繼承其他對象的,所以可以在任意一個對象中,使用 super 關鍵字。
4. 類的 prototype 屬性和__proto__屬性
大多數(shù)瀏覽器的 ES5 實現(xiàn)之中,每一個對象都有 __proto__ 屬性,指向對應的構造函數(shù)的 prototype 屬性。Class 作為構造函數(shù)的語法糖,同時有
prototype 屬性和 __proto__ 屬性,因此同時存在兩條繼承鏈。
(1)子類的 __proto__ 屬性,表示構造函數(shù)的繼承,總是指向父類。
(2)子類 prototype 屬性的 __proto__ 屬性,表示方法的繼承,總是指向父類的 prototype 屬性。
上面代碼中,子類 B 的 __proto__ 屬性指向父類 A ,子類 B 的 prototype 屬性的 __proto__ 屬性指向父類 A 的 prototype 屬性。
這樣的結果是因為,類的繼承是按照下面的模式實現(xiàn)的。
Object.setPrototypeOf 方法的實現(xiàn)。
Object.setPrototypeOf = function (obj, proto) { obj.__proto__ = proto; return obj; }因此,就得到了上面的結果
Object.setPrototypeOf(B.prototype, A.prototype); // 等同于 B.prototype.__proto__ = A.prototype; Object.setPrototypeOf(B, A); // 等同于 B.__proto__ = A;這兩條繼承鏈,可以這樣理解:作為一個對象,子類( B )的原型( __proto__ 屬性)是父類( A );作為一個構造函數(shù),子類( B )的原型對象
( prototype 屬性)是父類的原型對象( prototype 屬性)的實例。
4.1extends 的繼承目標
extends 關鍵字后面可以跟多種類型的值
class B extends A { }上面代碼的 A ,只要是一個有 prototype 屬性的函數(shù),就能被 B 繼承。由于函數(shù)都有 prototype 屬性(除了 Function.prototype 函數(shù)),因此 A 可以是任
意函數(shù)。
下面,討論三種特殊情況。
第一種特殊情況,子類繼承 Object 類。
這種情況下, A 其實就是構造函數(shù) Object 的復制, A 的實例就是 Object 的實例。
第二種特殊情況,不存在任何繼承。
這種情況下, A 作為一個基類(即不存在任何繼承),就是一個普通函數(shù),所以直接繼承 Function.prototype 。但是, A 調用后返回一個空對象(即
Object 實例),所以 A.prototype.__proto__ 指向構造函數(shù)( Object )的 prototype 屬性。
第三種特殊情況,子類繼承 null
這種情況與第二種情況非常像。 A 也是一個普通函數(shù),所以直接繼承 Function.prototype 。但是, A 調用后返回的對象不繼承任何方法,所以它的
__proto__ 指向 Function.prototype ,即實質上執(zhí)行了下面的代碼。
4.2實例的 __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上面代碼中, ColorPoint 繼承了 Point ,導致前者原型的原型是后者的原型。
因此,通過子類實例的 __proto__.__proto__ 屬性,可以修改父類實例的行為
上面代碼在 ColorPoint 的實例 p2 上向 Point 類添加方法,結果影響到了 Point 的實例 p1
5. 原生構造函數(shù)的繼承
原生構造函數(shù)是指語言內(nèi)置的構造函數(shù),通常用來生成數(shù)據(jù)結構。ECMAScript 的原生構造函數(shù)大致有下面這些。
Boolean()
Number()
String()
Array()
Date()
Function()
RegExp()
Error()
Object()
以前,這些原生構造函數(shù)是無法繼承的,比如,不能自己定義一個 Array 的子類。
上面代碼定義了一個繼承 Array 的 MyArray 類。但是,這個類的行為與 Array 完全不一致
var colors = new MyArray(); colors[0] = "red"; colors.length // 0 colors.length = 0; colors[0] // "red之所以會發(fā)生這種情況,是因為子類無法獲得原生構造函數(shù)的內(nèi)部屬性,通過 Array.apply() 或者分配給原型對象都不行。原生構造函數(shù)會忽略 apply 方
法傳入的 this ,也就是說,原生構造函數(shù)的 this 無法綁定,導致拿不到內(nèi)部屬性。
ES5 是先新建子類的實例對象 this ,再將父類的屬性添加到子類上,由于父類的內(nèi)部屬性無法獲取,導致無法繼承原生的構造函數(shù)。比如, Array 構造函
數(shù)有一個內(nèi)部屬性 [[DefineOwnProperty]] ,用來定義新屬性時,更新 length 屬性,這個內(nèi)部屬性無法在子類獲取,導致子類的 length 屬性行為不正
常。
下面的例子中,我們想讓一個普通對象繼承 Error 對象。
上面代碼中,我們想通過 Error.call(e) 這種寫法,讓普通對象 e 具有 Error 對象的實例屬性。但是, Error.call() 完全忽略傳入的第一個參數(shù),而是返
回一個新對象, e 本身沒有任何變化。這證明了 Error.call(e) 這種寫法,無法繼承原生構造函數(shù)。
ES6 允許繼承原生構造函數(shù)定義子類,因為 ES6 是先新建父類的實例對象 this ,然后再用子類的構造函數(shù)修飾 this ,使得父類的所有行為都可以繼
承。下面是一個繼承 Array 的例子。
上面代碼定義了一個 MyArray 類,繼承了 Array 構造函數(shù),因此就可以從 MyArray 生成數(shù)組的實例。這意味著,ES6 可以自定義原生數(shù)據(jù)結構(比如
Array 、 String 等)的子類,這是 ES5 無法做到的。
上面這個例子也說明, extends 關鍵字不僅可以用來繼承類,還可以用來繼承原生的構造函數(shù)。因此可以在原生數(shù)據(jù)結構的基礎上,定義自己的數(shù)據(jù)結
構。下面就是定義了一個帶版本功能的數(shù)組。
上面代碼中, VersionedArray 會通過 commit 方法,將自己的當前狀態(tài)生成一個版本快照,存入 history 屬性。 revert 方法用來將數(shù)組重置為最新一次保
存的版本。除此之外, VersionedArray 依然是一個普通數(shù)組,所有原生的數(shù)組方法都可以在它上面調用。
下面是一個自定義 Error 子類的例子,可以用來定制報錯時的行為。
注意,繼承 Object 的子類,有一個行為差異。
class NewObj extends Object{ constructor(){ super(...arguments); } } var o = new NewObj({attr: true}); o.attr === true // false上面代碼中, NewObj 繼承了 Object ,但是無法通過 super 方法向父類 Object 傳參。這是因為 ES6 改變了 Object 構造函數(shù)的行為,一旦發(fā)現(xiàn) Object 方
法不是通過 new Object() 這種形式調用,ES6 規(guī)定 Object 構造函數(shù)會忽略參數(shù)
6. Mixin 模式的實現(xiàn)
Mixin 指的是多個對象合成一個新的對象,新對象具有各個組成成員的接口。它的最簡單實現(xiàn)如下
const a = { a: 'a' }; const b = { b: 'b' }; const c = {...a, ...b}; // {a: 'a', b: 'b'}上面代碼中, c 對象是 a 對象和 b 對象的合成,具有兩者的接口。
下面是一個更完備的實現(xiàn),將多個類的接口“混入”(mix in)另一個類。
上面代碼的 mix 函數(shù),可以將多個對象合成為一個類。使用的時候,只要繼承這個類即可
class DistributedEdit extends mix(Loggable, Serializable) { // ... }總結
本博客源于本人閱讀相關書籍和視頻總結,創(chuàng)作不易,謝謝點贊支持。學到就是賺到。我是歌謠,勵志成為一名優(yōu)秀的技術革新人員。
歡迎私信交流,一起學習,一起成長。
推薦鏈接 其他文件目錄參照
“睡服“面試官系列之各系列目錄匯總(建議學習收藏)
?
總結
以上是生活随笔為你收集整理的“睡服”面试官系列第二十二篇之class的继承(建议收藏学习)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Qt播放器常用设置
- 下一篇: DB9,DB25,USB-A,USB-B