摘要:在繼承類中,相應的構造函數被標記為特殊的內部屬性。區別在于當一個普通的構造函數運行時,它會創建一個空對象作為,然后繼續運行。但是當派生的構造函數運行時,與上面說的不同,它指望父構造函數來完成這項工作。
原文 https://javascript.info/class...
Class 繼承與 superclass 可以 extends 自另一個 class。這是一個不錯的語法,技術上基于原型繼承。
要繼承一個對象,需要在 {..} 前指定 extends 和父對象。
這個 Rabbit 繼承自 Animal:
class Animal { constructor(name) { this.speed = 0; this.name = name; } run(speed) { this.speed += speed; alert(`${this.name} runs with speed ${this.speed}.`); } stop() { this.speed = 0; alert(`${this.name} stopped.`); } } // Inherit from Animal class Rabbit extends Animal { hide() { alert(`${this.name} hides!`); } } let rabbit = new Rabbit("White Rabbit"); rabbit.run(5); // White Rabbit runs with speed 5. rabbit.hide(); // White Rabbit hides!
如你所見,如你所想,extend 關鍵字實際上是在 Rabbit.prototype 添加 [Prototype]],引用到 Animal.prototype。
所以現在 rabbit 既可以訪問它自己的方法,也可以訪問 Animal 的方法。
extends 后可跟表達式Class 語法的 `extends" 后接的不限于指定一個類,更可以是表達式。
例如一個生成父類的函數:
function f(phrase) { return class { sayHi() { alert(phrase) } } } class User extends f("Hello") {} new User().sayHi(); // Hello
例子中,class User 繼承了 f("Hello")返回的結果。
對于高級編程模式,當我們使用的類是根據許多條件使用函數來生成時,這就很有用。
重寫一個方法現在讓我們進入下一步,重寫一個方法。到目前為止,Rabbit 從 Animal 繼承了 stop 方法,this.speed = 0。
如果我們在 Rabbit 中指定了自己的 stop,那么會被優先使用:
class Rabbit extends Animal { stop() { // ...this will be used for rabbit.stop() } }
......但通常我們不想完全替代父方法,而是在父方法的基礎上調整或擴展其功能。我們進行一些操作,讓它之前/之后或在過程中調用父方法。
Class 為此提供 super關鍵字。
使用 super.method(...) 調用父方法。
使用 super(...) 調用父構造函數(僅在 constructor 函數中)。
例如,讓兔子在 stop 時自動隱藏:
class Animal { constructor(name) { this.speed = 0; this.name = name; } run(speed) { this.speed += speed; alert(`${this.name} runs with speed ${this.speed}.`); } stop() { this.speed = 0; alert(`${this.name} stopped.`); } } class Rabbit extends Animal { hide() { alert(`${this.name} hides!`); } stop() { super.stop(); // call parent stop this.hide(); // and then hide } } let rabbit = new Rabbit("White Rabbit"); rabbit.run(5); // White Rabbit runs with speed 5. rabbit.stop(); // White Rabbit stopped. White rabbit hides!
現在,Rabbit 的 stop 方法通過 super.stop() 調用父類的方法。
箭頭函數無 super正如在 arrow-functions 一章中提到,箭頭函數沒有 super。
它會從外部函數中獲取 super。例如:
class Rabbit extends Animal { stop() { setTimeout(() => super.stop(), 1000); // call parent stop after 1sec } }
箭頭函數中的 super 與 stop() 中的相同,所以它按預期工作。如果我們在這里用普通函數,便會報錯:
// Unexpected super setTimeout(function() { super.stop() }, 1000);重寫構造函數
對于構造函數來說,這有點棘手 tricky。
直到現在,Rabbit 都沒有自己的 constructor。
Till now, Rabbit did not have its own constructor.
根據規范,如果一個類擴展了另一個類并且沒有 constructor ,那么會自動生成如下 constructor:
class Rabbit extends Animal { // generated for extending classes without own constructors constructor(...args) { super(...args); } }
我們可以看到,它調用了父 constructor 傳遞所有參數。如果我們不自己寫構造函數,就會發生這種情況。
現在我們將一個自定義構造函數添加到 Rabbit 中。除了name,我們還會設置 earLength:
class Animal { constructor(name) { this.speed = 0; this.name = name; } // ... } class Rabbit extends Animal { constructor(name, earLength) { this.speed = 0; this.name = name; this.earLength = earLength; } // ... } // Doesn"t work! let rabbit = new Rabbit("White Rabbit", 10); // Error: this is not defined.
哎呦出錯了!現在我們不能生成兔子了,為什么呢?
簡單來說:繼承類中的構造函數必須調用 super(...),(!)并且在使用 this 之前執行它。
...但為什么?這是什么情況?嗯...這個要求看起來確實奇怪。
現在我們探討細節,讓你真正理解其中緣由 ——
在JavaScript中,繼承了其他類的構造函數比較特殊。在繼承類中,相應的構造函數被標記為特殊的內部屬性 [[ConstructorKind]]:“derived”。
區別在于:
當一個普通的構造函數運行時,它會創建一個空對象作為 this,然后繼續運行。
但是當派生的構造函數運行時,與上面說的不同,它指望父構造函數來完成這項工作。
所以如果我們正在構造我們自己的構造函數,那么我們必須調用 super,否則具有 this 的對象將不被創建,并報錯。
對于 Rabbit 來說,我們需要在使用 this 之前調用 super(),如下所示:
class Animal { constructor(name) { this.speed = 0; this.name = name; } // ... } class Rabbit extends Animal { constructor(name, earLength) { super(name); this.earLength = earLength; } // ... } // now fine let rabbit = new Rabbit("White Rabbit", 10); alert(rabbit.name); // White Rabbit alert(rabbit.earLength); // 10Super 的實現與 [[HomeObject]]
讓我們再深入理解 super 的底層實現,我們會看到一些有趣的事情。
首先要說的是,以我們迄今為止學到的知識來看,實現 super 是不可能的。
那么思考一下,這是什么原理?當一個對象方法運行時,它將當前對象作為 this。如果我們調用 super.method(),那么如何檢索 method?很容易想到,我們需要從當前對象的原型中取出 method。從技術上講,我們(或JavaScript引擎)可以做到這一點嗎?
也許我們可以從 this 的 [[Prototype]] 中獲得方法,就像 this .__ proto __.method 一樣?不幸的是,這是行不通的。
讓我們試一試,簡單起見,我們不使用 class 了,直接使用普通對象。
在這里,rabbit.eat() 調用父對象的 animal.eat() 方法:
let animal = { name: "Animal", eat() { alert(`${this.name} eats.`); } }; let rabbit = { __proto__: animal, name: "Rabbit", eat() { // that"s how super.eat() could presumably work this.__proto__.eat.call(this); // (*) } }; rabbit.eat(); // Rabbit eats.
在 (*) 這一行,我們從原型(animal)中取出 eat,并以當前對象的上下文中調用它。請注意,.call(this) 在這里很重要,因為只寫 this .__ proto __.eat() 的話 eat 的調用對象將會是 animal,而不是當前對象。
以上代碼的 alert 是正確的。
但是現在讓我們再添加一個對象到原型鏈中,就要出事了:
let animal = { name: "Animal", eat() { alert(`${this.name} eats.`); } }; let rabbit = { __proto__: animal, eat() { // ...bounce around rabbit-style and call parent (animal) method this.__proto__.eat.call(this); // (*) } }; let longEar = { __proto__: rabbit, eat() { // ...do something with long ears and call parent (rabbit) method this.__proto__.eat.call(this); // (**) } }; longEar.eat(); // Error: Maximum call stack size exceeded
噢,完蛋!調用 longEar.eat() 報錯了!
這原因一眼可能看不透,但如果我們跟蹤 longEar.eat() 調用,大概就知道為什么了。在 (*) 和 (**) 兩行中, this 的值是當前對象(longEar)。重點來了:所有方法都將當前對象作為 this,而不是原型或其他東西。
因此,在兩行 (*) 和 (**) 中,this.__ proto__ 的值都是 rabbit。他們都調用了 rabbit.eat,于是就這么無限循環下去。
情況如圖:
1.在 longEar.eat() 里面,(**) 行中調用了 rabbit.eat,并且this = longEar。
// inside longEar.eat() we have this = longEar this.__proto__.eat.call(this) // (**) // becomes longEar.__proto__.eat.call(this) // that is rabbit.eat.call(this);
2.然后在rabbit.eat的 (*) 行中,我們希望傳到原型鏈的下一層,但是 this = longEar,所以 this .__ proto __.eat又是 rabbit.eat!
// inside rabbit.eat() we also have this = longEar this.__proto__.eat.call(this) // (*) // becomes longEar.__proto__.eat.call(this) // or (again) rabbit.eat.call(this);
...因此 rabbit.eat 在無盡循環調動,無法進入下一層。
這個問題不能簡單使用 this 解決。
[[HomeObject]]為了提供解決方案,JavaScript 為函數添加了一個特殊的內部屬性:[[HomeObject]]。
當函數被指定為類或對象方法時,其 [[HomeObject]] 屬性為該對象。
這實際上違反了 unbind 函數的思想,因為方法記住了它們的對象。并且 [[HomeObject]] 不能被改變,所以這是永久 bind(綁定)。所以在 JavaScript 這是一個很大的變化。
但是這種改變是安全的。 [[HomeObject]] 僅用于在 super 中獲取下一層原型。所以它不會破壞兼容性。
讓我們來看看它是如何在 super 中運作的:
let animal = { name: "Animal", eat() { // [[HomeObject]] == animal alert(`${this.name} eats.`); } }; let rabbit = { __proto__: animal, name: "Rabbit", eat() { // [[HomeObject]] == rabbit super.eat(); } }; let longEar = { __proto__: rabbit, name: "Long Ear", eat() { // [[HomeObject]] == longEar super.eat(); } }; longEar.eat(); // Long Ear eats.
每個方法都會在內部 [[HomeObject]] 屬性中記住它的對象。然后 super 使用它來解析原型。
在類和普通對象中定義的方法中都定義了 [[HomeObject]],但是對于對象,必須使用:method() 而不是 "method: function()"。
在下面的例子中,使用非方法語法(non-method syntax)進行比較。這么做沒有設置 [[HomeObject]] 屬性,繼承也不起作用:
let animal = { eat: function() { // should be the short syntax: eat() {...} // ... } }; let rabbit = { __proto__: animal, eat: function() { super.eat(); } }; rabbit.eat(); // Error calling super (because there"s no [[HomeObject]])靜態方法和繼承
class 語法也支持靜態屬性的繼承。
例如:
class Animal { constructor(name, speed) { this.speed = speed; this.name = name; } run(speed = 0) { this.speed += speed; alert(`${this.name} runs with speed ${this.speed}.`); } static compare(animalA, animalB) { return animalA.speed - animalB.speed; } } // Inherit from Animal class Rabbit extends Animal { hide() { alert(`${this.name} hides!`); } } let rabbits = [ new Rabbit("White Rabbit", 10), new Rabbit("Black Rabbit", 5) ]; rabbits.sort(Rabbit.compare); rabbits[0].run(); // Black Rabbit runs with speed 5.
現在我們可以調用 Rabbit.compare,假設繼承的 Animal.compare 將被調用。
它是如何工作的?再次使用原型。正如你猜到的那樣,extends 同樣給 Rabbit 提供了引用到 Animal的 [Prototype]。
所以,Rabbit 函數現在繼承 Animal 函數。Animal 自帶引用到 Function.prototype 的 [[Prototype]](因為它不 extend 其他類)。
看看這里:
class Animal {} class Rabbit extends Animal {} // for static propertites and methods alert(Rabbit.__proto__ === Animal); // true // and the next step is Function.prototype alert(Animal.__proto__ === Function.prototype); // true // that"s in addition to the "normal" prototype chain for object methods alert(Rabbit.prototype.__proto__ === Animal.prototype);
這樣 Rabbit 可以訪問 Animal 的所有靜態方法。
在內置對象中沒有靜態繼承請注意,內置類沒有靜態 [[Prototype]] 引用。例如,Object 具有 Object.defineProperty,Object.keys等方法,但 Array,Date 不會繼承它們。
Date 和 Object 的結構:
Date 和 Object 之間毫無關聯,他們獨立存在,不過 Date.prototype 繼承于 Object.prototype,僅此而已。
造成這個情況是因為 JavaScript 在設計初期沒有考慮使用 class 語法和繼承靜態方法。
原生拓展Array,Map 等內置類也可以擴展。
舉個例子,PowerArray 繼承自原生 Array:
// add one more method to it (can do more) class PowerArray extends Array { isEmpty() { return this.length === 0; } } let arr = new PowerArray(1, 2, 5, 10, 50); alert(arr.isEmpty()); // false let filteredArr = arr.filter(item => item >= 10); alert(filteredArr); // 10, 50 alert(filteredArr.isEmpty()); // false
請注意一件非常有趣的事情。像 filter,map 和其他內置方法 - 返回新的繼承類型的對象。他們依靠 constructor 屬性來做到這一點。
在上面的例子中,
arr.constructor === PowerArray
所以當調用 arr.filter() 時,它自動創建新的結果數組,就像 new PowerArray 一樣,于是我們可以繼續使用 PowerArray 的方法。
我們甚至可以自定義這種行為。如果存在靜態 getter Symbol.species,返回新建對象使用的 constructor。
下面的例子中,由于 Symbol.species 的存在,map,filter等內置方法將返回普通的數組:
class PowerArray extends Array { isEmpty() { return this.length === 0; } // built-in methods will use this as the constructor static get [Symbol.species]() { return Array; } } let arr = new PowerArray(1, 2, 5, 10, 50); alert(arr.isEmpty()); // false // filter creates new array using arr.constructor[Symbol.species] as constructor let filteredArr = arr.filter(item => item >= 10); // filteredArr is not PowerArray, but Array alert(filteredArr.isEmpty()); // Error: filteredArr.isEmpty is not a function
我們可以在其他 key 使用 Symbol.species,可以用于剝離結果值中的無用方法,或是增加其他方法。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/96024.html
摘要:請看對應版本干了什么可知,相當于以前在構造函數里的行為。這種寫法會與上文中寫法有何區別我們在環境下運行一下,看看這兩種構造函數的有何區別打印結果打印結果結合上文中關于原型的論述,仔細品味這兩者的差別,最好手動嘗試一下。 ES6 class 在ES6版本之前,JavaScript語言并沒有傳統面向對象語言的class寫法,ES6發布之后,Babel迅速跟進,廣大開發者也很快喜歡上ES6帶...
摘要:接下來我們看下類的寫法,這個就很接近于傳統面向對象語言了。如果你想了解傳統面向對象語言,這里是一個好切入點。作為對象時,指向父類的原型對象。這些就是為將來在中支持面向對象的類機制而預留的。 在ES5中,我們經常使用方法或者對象去模擬類的使用,并基于原型實現繼承,雖然可以實現功能,但是代碼并不優雅,很多人還是傾向于用 class 來組織代碼,很多類庫、框架創造了自己的 API 來實現 c...
摘要:使用類創建實例對象也是直接對類使用命令,跟中構造函數的用法一致。中沒有構造函數,作為構造函數的語法糖,同時有屬性和屬性,因此同時存在兩條繼承鏈。子類的屬性,表示構造函數的繼承,總是指向父類。 1 Class in ES6 ES6提出了類(Class)的概念,讓對象的原型的寫法更像面向對象語言寫法。 ES6中通過class定義對象,默認具有constructor方法和自定義方法,但是包含...
摘要:使用類創建實例對象也是直接對類使用命令,跟中構造函數的用法一致。中沒有構造函數,作為構造函數的語法糖,同時有屬性和屬性,因此同時存在兩條繼承鏈。子類的屬性,表示構造函數的繼承,總是指向父類。 1 Class in ES6 ES6提出了類(Class)的概念,讓對象的原型的寫法更像面向對象語言寫法。 ES6中通過class定義對象,默認具有constructor方法和自定義方法,但是包含...
類繼承 看類繼承前,先回顧構造函數怎么實現對象的繼承的 function F() { this.a = 1; } function Son() { F.call(this); } function inherit(S, F) { S.protot...
摘要:面向對象里最大的特點應該就屬繼承了。在第二篇文章里說過原型實例跟構造函數之間的繼承,并且還講了一道推算題。 通過上一篇文章想必各位老鐵已經熟悉了class了,這篇文章接著介紹繼承。面向對象里最大的特點應該就屬繼承了。一個項目可能需要不斷的迭代、完善、升級。那每一次的更新你是要重新寫呢,還是在原有的基礎上改吧改吧呢?當然,不是缺心眼的人肯定都會在原來的基礎上改吧改吧,那這個改吧改吧就需要...
閱讀 982·2021-11-23 09:51
閱讀 2695·2021-08-23 09:44
閱讀 656·2019-08-30 15:54
閱讀 1433·2019-08-30 13:53
閱讀 3101·2019-08-29 16:54
閱讀 2527·2019-08-29 16:26
閱讀 1186·2019-08-29 13:04
閱讀 2313·2019-08-26 13:50