創: 前端二牛
繼承是面向對象語言中一個最為人津津樂道的概念。許多面向對象語言都支持兩種繼承方式:接口繼承和實現繼承。接口繼承只繼承方法簽名,而實現繼承則繼承實際的方法。在ECMAScript中,函數沒有簽名,無法實現接口繼承,只支持實現繼承,繼承主要依靠的是原型鏈。
先來看一個簡單的原型鏈繼承示例,如下圖:
首先聲明一個 Human函數,給它的原型添加一個 getHumanSex方法。然后聲明一個 Person函數,要讓 Person繼承 Human,能使用 Human的屬性和方法,只要把 Person的原型重新賦值為 Human的一個實例就好了。最后給 Person的原型(也就是 Human的這個實例)添加一個 getPersonSex方法。現在創建 Person的一個實例 personObj, name='張三',age=12。如果訪問 personObj.name,那么就可以直接訪問到是 張三了。假如現在要訪問 humanSex,因為 personObj中沒有,所以會順著 __proto__屬性到 Person的原型中去找,而 Person的原型就是 Human的一個實例,它的 humanSex屬性就是 '男/女',所以 personObj.humanSex的值就是 '男/女',這就達到了繼承的效果。假如要訪問 getPersonSex, personObj中沒有,也是到原型中去找,找到了就調用,因為是使用 personObj.getPersonSex,所以在方法 getPersonSex中, this指的就是 personObj, returnthis.personSex就是返回 personObj.personSex,即為 '男'。 如果是調用 personObj.getHumanSex,還是先順著 __proto__到 Person的原型中去找, Person的原型是 Human的一個實例,里面沒有,那怎么辦呢,因為該原型是 Human的一個實例,所以它自然也會有個 __proto__指向 Human的原型,于是就繼續順著這個 __proto__到 Human的原型中去找,這就是所謂的原型鏈,找到了就開始調用,自然 getHumanSex中的 this指的也就是 personObj, returnthis.humanSex就是 returnpersonObj.humanSex,即 '男/女'。這就是原型鏈繼承。
細心的讀者也許會發現,在 Human的原型中也有個 __proto__,那么 Human的原型會不會也是某個函數的一個實例呢?如果我們調用 personObj.toString(),那么我們調用的究竟是誰的 toString()呢?要回答這個問題,我們來看下面這幅示意圖:
Human的原型其實是 Object的一個實例,自然 __proto__指向的就是 Object的原型,而 toString就是 Object原型上的方法。
其實ECMAScript中所有引用類型的默認原型都是 Object的實例,也就是說所有引用類型默認都繼承了 Object,而這個繼承正是通過原型鏈實現的。
原型的好處是所有的實例能共享它所包含的屬性和方法,也就是說不必在構造函數中定義對象實例的信息,但正如JavaScript創建對象(三)——原型模式中所說,這也帶來了一個問題。原型屬性會被所有實例共享,對于引用類型會出現一些應用時的共享問題,所以需要在構造函中,而不是在原型對象中定義屬性。但是在通過原型來實現繼承時,原型實際上就是另一個函數的實例。于是,原來的實例屬性也就順理成章地變成了現在的原型屬性了,那么原型共享數據的問題又出現了。
另外一個問題是,創建子類型的實例時,沒法向超類型的構造函數中傳遞參數。實際上,應該說是沒辦法在不影響所有對象的實例下,給超類型的構造函數傳遞參數。有鑒于此,實踐中很少會單獨使用原型鏈。
文實例總結了JavaScript類的繼承操作。分享給大家供大家參考,具體如下:
一、類式繼承
首先要做的是創建構造函數。按慣例,其名稱就是類名,首字母應該大寫。在構造函數中,創建實例屬性要用關鍵字this 。類的方法則被添加到prototype對象中。要創建該類的實例,只需結合關鍵字new調用這構造函數即可。
/* Class Person. */ function Person(name) { this.name=name; } Person.prototype.getName=function() { return this.name; } var reader=new Person('John Smith'); reader.getName();
二、原型鏈
JavaScript的每個對象都有一個名為prototype的屬性,這個屬性要么指向另一個對象,要么是null.在訪問對象的某個成員時,如果這個成員未見于當前對象,那么就會到prototype所指的對象中去查找。如果還是沒有找到,那么就會沿著原型鏈逐一訪問每個原型對象,直到找到這個成員。這意味著讓一個類繼承另一個類,只需將子類的prototype設置為超類的一個實例即可。
為了讓Author繼承Person,必須手工將Author的prototype設置為Person的一個實例。最后一步是將prototype的construct屬性重設為Author(因為prototype屬性設置為Person的實例)時,其construct屬性被抹除了。
function Author(name, books) { Person.call(this, name); // Call the superclass' constructor in the scope of this. this.books=books; // Add an attribute to Author. } Author.prototype=new Person(); // Set up the prototype chain. Author.prototype.constructor=Author; // Set the constructor attribute to Author. Author.prototype.getBooks=function() { // Add a method to Author. return this.books; }; var author=[]; author[0]=new Author('Dustin Diaz', ['JavaScript Design Patterns']); author[1]=new Author('Ross Harmes', ['JavaScript Design Patterns']); console.log(author[1].getName()); console.log(author[1].getBooks());
三、extend函數
為了簡化類的聲明,可以把派生子類的整個過程包裝在一個名為extend的函數中。它的作用與其他語言的extend關鍵字類似,即基于一個給定的類的結構創建一個新的類:
function extend(subClass, superClass) { var F=function() {}; F.prototype=superClass.prototype; subClass.prototype=new F(); subClass.prototype.constructor=subClass; }
其實所做的事與之前的是一樣的。它先設置了prototype,然后再將其constructor重設為恰當的值。并且中間利用了一個空函數,這樣就可以避免創建超類的實例。使用extend繼承的寫法:
function Person(name) { this.name=name; } Person.prototype.getName=function() { return this.name; } /* Class Author. */ function Author(name, books) { Person.call(this, name); this.books=books; } extend(Author, Person); Author.prototype.getBooks=function() { return this.books; };
但上面的存在一個問題就是超類Person的名稱被固化在Author類的聲明當中。更普世性的做法應該像下面這樣:
/* Extend function, improved. */ function extend(subClass, superClass) { var F=function() {}; F.prototype=superClass.prototype; subClass.prototype=new F(); subClass.prototype.constructor=subClass; subClass.superclass=superClass.prototype; if(superClass.prototype.constructor==Object.prototype.constructor) { superClass.prototype.constructor=superClass; } } /* Class Author. */ function Author(name, books) { Author.superclass.constructor.call(this, name); this.books=books; } extend(Author, Person); Author.prototype.getBooks=function() { return this.books; }; Author.prototype.getName=function() { var name=Author.superclass.getName.call(this); return name + ', Author of ' + this.getBooks().join(', '); };
這個extend改進之后,多了一個superclass的屬性,這個屬性可以弱化Author和Person之間的耦合。extend后面三行用來確保超類的construtor已經被正確設置了。有了superclass的屬性,就可以直接調用超類中的方法。這在既要重新定義超類的某個方法而又想訪問其在超類中的實現時可以派上用場。例如,為了用一個新的getName的方法重新定義Person類中的同名方法,你可以先用Author.superclass.getName獲得作者的名字,然后再次基礎上添加新的信息。
四、原型繼承
原型式繼承與類式繼承截然不同,我們在學習他的時候,最好忘記自己關于類和實例的一切知識,只從對象的角度來思考。使用原型式繼承時,并不需要用類來定義對象的結構,只需直接創建一個對像就可以。這個對象隨后可以被新的對象使用,該對象被稱為原型對象。
下面使用原型對象來重新設計上面Person和Author:
var Person={ name: 'default name', getName: function() { return this.name; } }; var reader=clone(Person); alert(reader.getName()); // This will output 'default name'. reader.name='John Smith'; alert(reader.getName()); // This will now output 'John Smith'.
clone函數可以用來創建新的類Person對象,創建一個空對象,并且該對象的原型對象被設置為person。當新對象中找不到某個方法時就會在原型對象中查找。
你不必去為了創建Author而定義一個Person子類,只要執行一次克隆就可以:
var Author=clone(Person); Author.books=[]; // Default value. Author.getBooks=function() { return this.books; }
然后你可以重定義該克隆中的方法和屬性。可以修改Person的默認值。也可以添加新的屬性和方法。這樣一來就創建了一個新的原型對象,你可以將其用于創建新的Author對象:
var author=[]; author[0]=clone(Author); author[0].name='Dustin Diaz'; author[0].books=['JavaScript Design Patterns']; author[1]=clone(Author); author[1].name='Ross Harmes'; author[1].books=['JavaScript Design Patterns']; author[1].getName(); author[1].getBooks();
clone函數的寫法:
function clone(object) { function F() {} F.prototype=object; return new F; }
五、原型繼承和類式繼承之間的比較
可以自己去總結、
從內存,適用范圍,優缺點等方面去分析
六、摻元類
有一種重用代碼的方法不需要用到嚴格的繼承,如果想把一個函數運用到多個類當中,可以通過擴充的方法讓這些類共享函數。其實際大體做法就是:先創建一個包含各種通用的方法類,然后再擴充其他類,這種包含通用方法類稱為摻元類,他們通常不會被實例化和直接調用,其存在的目的是向其他類提供自己的方法。
var Mixin=function() {}; Mixin.prototype={ serialize: function() { var output=[]; for(key in this) { output.push(key + ': ' + this[key]); } return output.join(', '); } }; augment(Author, Mixin); var author=new Author('Ross Harmes', ['JavaScript Design Patterns']); var serializedString=author.serialize(); function augment(receivingClass, givingClass) { for(methodName in givingClass.prototype) { if(!receivingClass.prototype[methodName]) { receivingClass.prototype[methodName]=givingClass.prototype[methodName]; } } }
但是有時候你并不需要所有的方法,因此我們還需要提供額外的參數來選擇我們所需要的方法。如果不提供,那就全部復制。
function augment(receivingClass, givingClass) { if(arguments[2]) { // Only give certain methods. for(var i=2, len=arguments.length; i < len; i++) { receivingClass.prototype[arguments[i]]=givingClass.prototype[arguments[i]]; } } else { // Give all methods. for(methodName in givingClass.prototype) { if(!receivingClass.prototype[methodName]) { receivingClass.prototype[methodName]=givingClass.prototype[methodName]; } } } }
以下是總結出來最全前端框架視頻,包含: javascript/vue/react/angualrde/express/koa/webpack 等學習資料。
家好,很高興又見面了,我是姜茶的編程筆記,我們一起學習前端相關領域技術,共同進步,也歡迎大家關注、點贊、收藏、轉發,您的支持是我不斷創作的動力
面試官:如何理解繼承和原型鏈?
我(屌毛):巴拉巴拉一堆,看下面
JavaScript 中的繼承是通過原型鏈實現的,這種方式通常稱為原型繼承。與傳統的類繼承不同,JavaScript 的繼承基于對象。
// 構造函數
function Animal(name) {
this.name=name;
}
// 方法定義
Animal.prototype.sayName=function() {
console.log('My name is ' + this.name);
};
// 創建子對象
function Dog(name, breed) {
Animal.call(this, name); // 調用父構造函數以初始化父屬性
this.breed=breed;
}
// 設置原型鏈
Dog.prototype=Object.create(Animal.prototype);
Dog.prototype.constructor=Dog; // 修正構造函數引用
// 添加子對象方法
Dog.prototype.bark=function() {
console.log('Woof! I am a ' + this.breed);
};
// 創建實例
var myDog=new Dog('Buddy', 'Golden Retriever');
// 調用方法
myDog.sayName(); // 輸出: My name is Buddy
myDog.bark(); // 輸出: Woof! I am a Golden Retriever
在這個例子中:
如果你有任何問題或建議,歡迎在評論區留言交流!Happy coding!
*請認真填寫需求信息,我們會在24小時內與您取得聯系。