摘要:在繼承類中,相應(yīng)的構(gòu)造函數(shù)被標(biāo)記為特殊的內(nèi)部屬性。區(qū)別在于當(dāng)一個(gè)普通的構(gòu)造函數(shù)運(yùn)行時(shí),它會(huì)創(chuàng)建一個(gè)空對(duì)象作為,然后繼續(xù)運(yùn)行。但是當(dāng)派生的構(gòu)造函數(shù)運(yùn)行時(shí),與上面說的不同,它指望父構(gòu)造函數(shù)來完成這項(xiàng)工作。
原文 https://javascript.info/class...
Class 繼承與 superclass 可以 extends 自另一個(gè) class。這是一個(gè)不錯(cuò)的語法,技術(shù)上基于原型繼承。
要繼承一個(gè)對(duì)象,需要在 {..} 前指定 extends 和父對(duì)象。
這個(gè) 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 關(guān)鍵字實(shí)際上是在 Rabbit.prototype 添加 [Prototype]],引用到 Animal.prototype。
所以現(xiàn)在 rabbit 既可以訪問它自己的方法,也可以訪問 Animal 的方法。
extends 后可跟表達(dá)式Class 語法的 `extends" 后接的不限于指定一個(gè)類,更可以是表達(dá)式。
例如一個(gè)生成父類的函數(shù):
function f(phrase) { return class { sayHi() { alert(phrase) } } } class User extends f("Hello") {} new User().sayHi(); // Hello
例子中,class User 繼承了 f("Hello")返回的結(jié)果。
對(duì)于高級(jí)編程模式,當(dāng)我們使用的類是根據(jù)許多條件使用函數(shù)來生成時(shí),這就很有用。
重寫一個(gè)方法現(xiàn)在讓我們進(jìn)入下一步,重寫一個(gè)方法。到目前為止,Rabbit 從 Animal 繼承了 stop 方法,this.speed = 0。
如果我們?cè)?Rabbit 中指定了自己的 stop,那么會(huì)被優(yōu)先使用:
class Rabbit extends Animal { stop() { // ...this will be used for rabbit.stop() } }
......但通常我們不想完全替代父方法,而是在父方法的基礎(chǔ)上調(diào)整或擴(kuò)展其功能。我們進(jìn)行一些操作,讓它之前/之后或在過程中調(diào)用父方法。
Class 為此提供 super關(guān)鍵字。
使用 super.method(...) 調(diào)用父方法。
使用 super(...) 調(diào)用父構(gòu)造函數(shù)(僅在 constructor 函數(shù)中)。
例如,讓兔子在 stop 時(shí)自動(dòng)隱藏:
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!
現(xiàn)在,Rabbit 的 stop 方法通過 super.stop() 調(diào)用父類的方法。
箭頭函數(shù)無 super正如在 arrow-functions 一章中提到,箭頭函數(shù)沒有 super。
它會(huì)從外部函數(shù)中獲取 super。例如:
class Rabbit extends Animal { stop() { setTimeout(() => super.stop(), 1000); // call parent stop after 1sec } }
箭頭函數(shù)中的 super 與 stop() 中的相同,所以它按預(yù)期工作。如果我們?cè)谶@里用普通函數(shù),便會(huì)報(bào)錯(cuò):
// Unexpected super setTimeout(function() { super.stop() }, 1000);重寫構(gòu)造函數(shù)
對(duì)于構(gòu)造函數(shù)來說,這有點(diǎn)棘手 tricky。
直到現(xiàn)在,Rabbit 都沒有自己的 constructor。
Till now, Rabbit did not have its own constructor.
根據(jù)規(guī)范,如果一個(gè)類擴(kuò)展了另一個(gè)類并且沒有 constructor ,那么會(huì)自動(dòng)生成如下 constructor:
class Rabbit extends Animal { // generated for extending classes without own constructors constructor(...args) { super(...args); } }
我們可以看到,它調(diào)用了父 constructor 傳遞所有參數(shù)。如果我們不自己寫構(gòu)造函數(shù),就會(huì)發(fā)生這種情況。
現(xiàn)在我們將一個(gè)自定義構(gòu)造函數(shù)添加到 Rabbit 中。除了name,我們還會(huì)設(shè)置 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.
哎呦出錯(cuò)了!現(xiàn)在我們不能生成兔子了,為什么呢?
簡(jiǎn)單來說:繼承類中的構(gòu)造函數(shù)必須調(diào)用 super(...),(!)并且在使用 this 之前執(zhí)行它。
...但為什么?這是什么情況?嗯...這個(gè)要求看起來確實(shí)奇怪。
現(xiàn)在我們探討細(xì)節(jié),讓你真正理解其中緣由 ——
在JavaScript中,繼承了其他類的構(gòu)造函數(shù)比較特殊。在繼承類中,相應(yīng)的構(gòu)造函數(shù)被標(biāo)記為特殊的內(nèi)部屬性 [[ConstructorKind]]:“derived”。
區(qū)別在于:
當(dāng)一個(gè)普通的構(gòu)造函數(shù)運(yùn)行時(shí),它會(huì)創(chuàng)建一個(gè)空對(duì)象作為 this,然后繼續(xù)運(yùn)行。
但是當(dāng)派生的構(gòu)造函數(shù)運(yùn)行時(shí),與上面說的不同,它指望父構(gòu)造函數(shù)來完成這項(xiàng)工作。
所以如果我們正在構(gòu)造我們自己的構(gòu)造函數(shù),那么我們必須調(diào)用 super,否則具有 this 的對(duì)象將不被創(chuàng)建,并報(bào)錯(cuò)。
對(duì)于 Rabbit 來說,我們需要在使用 this 之前調(diào)用 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 的實(shí)現(xiàn)與 [[HomeObject]]
讓我們?cè)偕钊肜斫?super 的底層實(shí)現(xiàn),我們會(huì)看到一些有趣的事情。
首先要說的是,以我們迄今為止學(xué)到的知識(shí)來看,實(shí)現(xiàn) super 是不可能的。
那么思考一下,這是什么原理?當(dāng)一個(gè)對(duì)象方法運(yùn)行時(shí),它將當(dāng)前對(duì)象作為 this。如果我們調(diào)用 super.method(),那么如何檢索 method?很容易想到,我們需要從當(dāng)前對(duì)象的原型中取出 method。從技術(shù)上講,我們(或JavaScript引擎)可以做到這一點(diǎn)嗎?
也許我們可以從 this 的 [[Prototype]] 中獲得方法,就像 this .__ proto __.method 一樣?不幸的是,這是行不通的。
讓我們?cè)囈辉?,?jiǎn)單起見,我們不使用 class 了,直接使用普通對(duì)象。
在這里,rabbit.eat() 調(diào)用父對(duì)象的 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,并以當(dāng)前對(duì)象的上下文中調(diào)用它。請(qǐng)注意,.call(this) 在這里很重要,因?yàn)橹粚?this .__ proto __.eat() 的話 eat 的調(diào)用對(duì)象將會(huì)是 animal,而不是當(dāng)前對(duì)象。
以上代碼的 alert 是正確的。
但是現(xiàn)在讓我們?cè)偬砑右粋€(gè)對(duì)象到原型鏈中,就要出事了:
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
噢,完蛋!調(diào)用 longEar.eat() 報(bào)錯(cuò)了!
這原因一眼可能看不透,但如果我們跟蹤 longEar.eat() 調(diào)用,大概就知道為什么了。在 (*) 和 (**) 兩行中, this 的值是當(dāng)前對(duì)象(longEar)。重點(diǎn)來了:所有方法都將當(dāng)前對(duì)象作為 this,而不是原型或其他東西。
因此,在兩行 (*) 和 (**) 中,this.__ proto__ 的值都是 rabbit。他們都調(diào)用了 rabbit.eat,于是就這么無限循環(huán)下去。
情況如圖:
1.在 longEar.eat() 里面,(**) 行中調(diào)用了 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 在無盡循環(huán)調(diào)動(dòng),無法進(jìn)入下一層。
這個(gè)問題不能簡(jiǎn)單使用 this 解決。
[[HomeObject]]為了提供解決方案,JavaScript 為函數(shù)添加了一個(gè)特殊的內(nèi)部屬性:[[HomeObject]]。
當(dāng)函數(shù)被指定為類或?qū)ο蠓椒〞r(shí),其 [[HomeObject]] 屬性為該對(duì)象。
這實(shí)際上違反了 unbind 函數(shù)的思想,因?yàn)榉椒ㄓ涀×怂鼈兊膶?duì)象。并且 [[HomeObject]] 不能被改變,所以這是永久 bind(綁定)。所以在 JavaScript 這是一個(gè)很大的變化。
但是這種改變是安全的。 [[HomeObject]] 僅用于在 super 中獲取下一層原型。所以它不會(huì)破壞兼容性。
讓我們來看看它是如何在 super 中運(yùn)作的:
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.
每個(gè)方法都會(huì)在內(nèi)部 [[HomeObject]] 屬性中記住它的對(duì)象。然后 super 使用它來解析原型。
在類和普通對(duì)象中定義的方法中都定義了 [[HomeObject]],但是對(duì)于對(duì)象,必須使用:method() 而不是 "method: function()"。
在下面的例子中,使用非方法語法(non-method syntax)進(jìn)行比較。這么做沒有設(shè)置 [[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]])靜態(tài)方法和繼承
class 語法也支持靜態(tài)屬性的繼承。
例如:
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.
現(xiàn)在我們可以調(diào)用 Rabbit.compare,假設(shè)繼承的 Animal.compare 將被調(diào)用。
它是如何工作的?再次使用原型。正如你猜到的那樣,extends 同樣給 Rabbit 提供了引用到 Animal的 [Prototype]。
所以,Rabbit 函數(shù)現(xiàn)在繼承 Animal 函數(shù)。Animal 自帶引用到 Function.prototype 的 [[Prototype]](因?yàn)樗?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 的所有靜態(tài)方法。
在內(nèi)置對(duì)象中沒有靜態(tài)繼承請(qǐng)注意,內(nèi)置類沒有靜態(tài) [[Prototype]] 引用。例如,Object 具有 Object.defineProperty,Object.keys等方法,但 Array,Date 不會(huì)繼承它們。
Date 和 Object 的結(jié)構(gòu):
Date 和 Object 之間毫無關(guān)聯(lián),他們獨(dú)立存在,不過 Date.prototype 繼承于 Object.prototype,僅此而已。
造成這個(gè)情況是因?yàn)?JavaScript 在設(shè)計(jì)初期沒有考慮使用 class 語法和繼承靜態(tài)方法。
原生拓展Array,Map 等內(nèi)置類也可以擴(kuò)展。
舉個(gè)例子,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
請(qǐng)注意一件非常有趣的事情。像 filter,map 和其他內(nèi)置方法 - 返回新的繼承類型的對(duì)象。他們依靠 constructor 屬性來做到這一點(diǎn)。
在上面的例子中,
arr.constructor === PowerArray
所以當(dāng)調(diào)用 arr.filter() 時(shí),它自動(dòng)創(chuàng)建新的結(jié)果數(shù)組,就像 new PowerArray 一樣,于是我們可以繼續(xù)使用 PowerArray 的方法。
我們甚至可以自定義這種行為。如果存在靜態(tài) getter Symbol.species,返回新建對(duì)象使用的 constructor。
下面的例子中,由于 Symbol.species 的存在,map,filter等內(nèi)置方法將返回普通的數(shù)組:
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,可以用于剝離結(jié)果值中的無用方法,或是增加其他方法。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/96024.html
摘要:請(qǐng)看對(duì)應(yīng)版本干了什么可知,相當(dāng)于以前在構(gòu)造函數(shù)里的行為。這種寫法會(huì)與上文中寫法有何區(qū)別我們?cè)诃h(huán)境下運(yùn)行一下,看看這兩種構(gòu)造函數(shù)的有何區(qū)別打印結(jié)果打印結(jié)果結(jié)合上文中關(guān)于原型的論述,仔細(xì)品味這兩者的差別,最好手動(dòng)嘗試一下。 ES6 class 在ES6版本之前,JavaScript語言并沒有傳統(tǒng)面向?qū)ο笳Z言的class寫法,ES6發(fā)布之后,Babel迅速跟進(jìn),廣大開發(fā)者也很快喜歡上ES6帶...
摘要:接下來我們看下類的寫法,這個(gè)就很接近于傳統(tǒng)面向?qū)ο笳Z言了。如果你想了解傳統(tǒng)面向?qū)ο笳Z言,這里是一個(gè)好切入點(diǎn)。作為對(duì)象時(shí),指向父類的原型對(duì)象。這些就是為將來在中支持面向?qū)ο蟮念悪C(jī)制而預(yù)留的。 在ES5中,我們經(jīng)常使用方法或者對(duì)象去模擬類的使用,并基于原型實(shí)現(xiàn)繼承,雖然可以實(shí)現(xiàn)功能,但是代碼并不優(yōu)雅,很多人還是傾向于用 class 來組織代碼,很多類庫(kù)、框架創(chuàng)造了自己的 API 來實(shí)現(xiàn) c...
摘要:使用類創(chuàng)建實(shí)例對(duì)象也是直接對(duì)類使用命令,跟中構(gòu)造函數(shù)的用法一致。中沒有構(gòu)造函數(shù),作為構(gòu)造函數(shù)的語法糖,同時(shí)有屬性和屬性,因此同時(shí)存在兩條繼承鏈。子類的屬性,表示構(gòu)造函數(shù)的繼承,總是指向父類。 1 Class in ES6 ES6提出了類(Class)的概念,讓對(duì)象的原型的寫法更像面向?qū)ο笳Z言寫法。 ES6中通過class定義對(duì)象,默認(rèn)具有constructor方法和自定義方法,但是包含...
摘要:使用類創(chuàng)建實(shí)例對(duì)象也是直接對(duì)類使用命令,跟中構(gòu)造函數(shù)的用法一致。中沒有構(gòu)造函數(shù),作為構(gòu)造函數(shù)的語法糖,同時(shí)有屬性和屬性,因此同時(shí)存在兩條繼承鏈。子類的屬性,表示構(gòu)造函數(shù)的繼承,總是指向父類。 1 Class in ES6 ES6提出了類(Class)的概念,讓對(duì)象的原型的寫法更像面向?qū)ο笳Z言寫法。 ES6中通過class定義對(duì)象,默認(rèn)具有constructor方法和自定義方法,但是包含...
類繼承 看類繼承前,先回顧構(gòu)造函數(shù)怎么實(shí)現(xiàn)對(duì)象的繼承的 function F() { this.a = 1; } function Son() { F.call(this); } function inherit(S, F) { S.protot...
摘要:面向?qū)ο罄镒畲蟮奶攸c(diǎn)應(yīng)該就屬繼承了。在第二篇文章里說過原型實(shí)例跟構(gòu)造函數(shù)之間的繼承,并且還講了一道推算題。 通過上一篇文章想必各位老鐵已經(jīng)熟悉了class了,這篇文章接著介紹繼承。面向?qū)ο罄镒畲蟮奶攸c(diǎn)應(yīng)該就屬繼承了。一個(gè)項(xiàng)目可能需要不斷的迭代、完善、升級(jí)。那每一次的更新你是要重新寫呢,還是在原有的基礎(chǔ)上改吧改吧呢?當(dāng)然,不是缺心眼的人肯定都會(huì)在原來的基礎(chǔ)上改吧改吧,那這個(gè)改吧改吧就需要...
閱讀 991·2021-11-23 09:51
閱讀 2704·2021-08-23 09:44
閱讀 667·2019-08-30 15:54
閱讀 1440·2019-08-30 13:53
閱讀 3115·2019-08-29 16:54
閱讀 2533·2019-08-29 16:26
閱讀 1200·2019-08-29 13:04
閱讀 2327·2019-08-26 13:50