JS中的繼承(原型鏈、構造函數、組合式、class類)
1、繼承
應注意區分繼承和實例化,實例化是生成一個對象,這個對象具有構造函數的屬性和方法;繼承指的應該是利用父類生成一個新的子類構造函數,通過這個子類構造函數實例化的對象,具有子類的屬性和方法,同時也具有父類的屬性和方法。
2、原型鏈繼承
2.1、實現方法
實現原型鏈繼承的方法是通過重寫子類的原型對象(比如 Student.prototype )的值為父類(比如Person) 的一個實例,由此可以實現繼承(Student 繼承了 Person ) 。
Son.prototype = new Parent();
代碼示例:
//父類:人 function Person() { this.head = "腦袋瓜子"; } //子類:學生,繼承了“人”這個類 function Student(studentID) { this.studentID = studentID; } Student.prototype = new Person(); var stu1 = new Student(1001); console.log(stu1.head); //腦袋瓜子 stu1.head = "聰明的腦袋瓜子"; console.log(stu1.head); //聰明的腦袋瓜子 var stu2 = new Student(1002); console.log(stu2.head); //腦袋瓜子
上面代碼中,student 繼承了 person,所以 stu1 能訪問到父類 Person 上定義的 head 屬性。
2.2、原型鏈繼承的弊端(誤修改原型對象上的引用類型值)
原型鏈繼承的缺點有:
(1)引用類型的屬性被所有實例共享,在某個實例上修改引用類型的值,會導致其他實例也受到影響
(2)在實現繼承時,子類無法向父類傳參數
當實例對象中存在和原型對象上同名的屬性時,會自動屏蔽原型對象上的同名屬性。stu1.head = "聰明的腦袋瓜子" 實際上只是給 stu1 添加了一個本地屬性 head 并設置了相關值。所以當我們打印 stu1.head 時,訪問的是該實例的本地屬性,而不是其原型對象上的 head 屬性(它因和本地屬性名同名已經被屏蔽了)。
原型對象上基本類型的值,都不會被實例所重寫/覆蓋。在實例上設置與原型對象上同名屬性的值,只會在實例上創建一個同名的本地屬性。但是,原型對象上引用類型的值可以通過實例進行修改,致使所有實例共享著的該引用類型的值也會隨之改變,這正是原型鏈繼承的最大缺點。
//父類:人 function Person () { this.head = '腦袋瓜子'; this.emotion = ['喜']; this.say = function () { console.log('hi'); } } //子類:學生,繼承了“人”這個類 function Student(studentID) { this.studentID = studentID; } Student.prototype = new Person(); var stu1 = new Student(1001); console.log(stu1.emotion); //['喜'] stu1.emotion.push('怒'); console.log(stu1.emotion); //["喜", "怒"] var stu2 = new Student(1002); console.log(stu2.emotion); //["喜", "怒"] console.log(stu1.say === stu2.say); //true 證明子類的實例對象共享引用類型的值
我們在 Person 類中添加了一個 emotion 屬性,它是一個引用類型的值。可以看到,此時如果一個實例不小心修改了原型對象上引用類型的值,會導致其它實例也跟著受影響。從上面的代碼可知,在實現繼承時,子類并不能向父類傳遞參數。
參考:http://www.rzrgm.cn/sarahwang/p/6879161.html
3、構造函數實現繼承
借用構造函數實現的繼承可以避免原型鏈繼承會導致誤修改原型對象上引用類型值的缺點。
3.1、實現方法
構造函數實現繼承的方法是在子類的構造函數中,通過 apply() 或 call() 的形式來調用父類的構造函數,以實現繼承。
function Son(){ Parent.call(this); }
代碼示例:
//父類:人 function Person (headMsg) { this.head = headMsg; this.emotion = ['喜', '怒']; this.say = function () { console.log('hi'); } } //子類:學生,繼承了“人”這個類 function Student(studentID) { this.studentID = studentID; Person.call(this, '腦袋瓜子'); //構造函數在實現繼承時可以傳遞參數 } var stu1 = new Student(1001); console.log(stu1.emotion); //['喜', '怒'] stu1.emotion.push('哀'); console.log(stu1.emotion); //["喜", "怒", "哀"] var stu2 = new Student(1002); console.log(stu2.emotion); //["喜", "怒"] console.log(stu1.say === stu2.say); //false 證明與原型鏈繼承不同,引用類型的值并不共享
構造函數繼承相對于原型鏈繼承來說只是去掉了之前通過 prototype 繼承的方法,而采用了 Person.call (this) 的形式實現繼承,通過 call 來指定父類構造函數的作用域。
this 指向解析:
可以簡單理解為:誰調用它,它就指向誰。在 stu1 = new Student ( ) 構造函數時,是 stu1 調用 Student 方法,所以其內部 this 的值指向的是 stu1,所以 Person.call ( this ) 就相當于Person.call ( stu1 ),就相當于 stu1.Person( )。最后,stu1 去調用 Person 方法時,Person 內部的 this 指向就指向了 stu1。那么Person 內部this 上的所有屬性和方法,都被拷貝到了 stu1 上。stu2 也是同理,所以其實是,每個實例都具有自己的 emotion 屬性副本,它們互不影響。
所以,通過構造函數來實現繼承,每個示例都會具有屬性及方法的副本,互相不影響,由此也避免了原型鏈繼承的弊端,即避免了引用類型的屬性被所有實例共享。
參考:http://www.rzrgm.cn/sarahwang/p/6879161.html
3.2、構造函數繼承的弊端(占用內存)
這種形式的繼承,每個子類實例都會具有屬性及方法的副本,互相不影響,這樣就避免了原型鏈繼承的弊端。但是這樣做會有以下的缺點:
(1)每個實例都拷貝一份屬性和方法的副本,占用內存大,尤其是方法過多的時候。(函數復用又無從談起了,本來我們用 prototype 就是解決復用問題的)
(2)方法都作為了實例自己的方法,當需求改變,要改動其中的一個方法時,之前所有的實例,他們的該方法都不能及時作出更新。只有后面的實例才能訪問到新方法。
所以說,無論是單獨使用原型鏈繼承還是借用構造函數繼承都有很大的缺點,最好的辦法是,將兩者結合一起使用,這就是下面介紹的組合式繼承。
4、組合式繼承(融合了優點,最常用的繼承)
組合式繼承融合了原型鏈繼承和構造函數的優點,是 JavaScript 中最常用的繼承模式。
4.1、實現方法
組合式繼承實現方式是將需要共享的方法寫在父類的原型對象上,而需要每個實例都拷貝一份的屬性則寫在父類的構造函數上,由此可以需要共享的方法能實現實例間共享,需要自己維護的屬性能實現每個實例都有具有自己的副本而不會導致誤修改,避免了原型鏈繼承和構造函數繼承的弊端。
//父類:人 function Person () { this.head = '腦袋瓜子'; this.emotion = ['喜']; //人都有喜怒哀樂 } //將 Person 類中需共享的方法放到 prototype 中,實現復用 Person.prototype.say = function () { console.log('hi'); } //子類:學生,繼承了“人”這個類 function Student(studentID) { this.studentID = studentID; Person.call(this); //構造函數繼承的方法 } Student.prototype = new Person(); //原型鏈繼承的方法 此時 Student.prototype 中的 constructor 被重寫了,會導致 stu1.constructor === Person Student.prototype.constructor = Student; //將 Student 原型對象的 constructor 指針重新指向 Student 本身 var stu1 = new Student(1001); console.log(stu1.emotion); //['喜'] stu1.emotion.push('怒'); console.log(stu1.emotion); //["喜", "怒"] var stu2 = new Student(1002); console.log(stu2.emotion); //["喜"] 需要實例各自維護一份的屬性不會被誤修改 stu1.say(); //hi console.log(stu1.say === stu2.say) //true 證明函數實現了共享 console.log(stu1.constructor); //Student 實例的構造函數仍然是子類構造函數 Student,而不是父類 Person
將 Person 類中需要復用的方法提取到 Person.prototype 中,然后設置 Student 的原型對象為 Person 類的一個實例,這樣 stu1 就能訪問到 Person 原型對象上的屬性和方法了。其次,為保證 stu1 和 stu2 擁有各自的父類屬性副本,我們在 Student 構造函數中,還是使用了 Person.call ( this ) 方法。如此,結合原型鏈繼承和借用構造函數繼承,就完美地解決了之前這二者各自表現出來的缺點。
參考:http://www.rzrgm.cn/sarahwang/p/9098044.html
5、類實現繼承(class、extends)
在 ES6 中,可以通過 class 和 extends 關鍵字來實現繼承。ES6 中類實現繼承可以看做是組合式繼承的語法糖(簡單理解),但兩者的繼承機制還是不太一樣的。
class Animal { constructor(age) { this.age = age; } say() { console.log("hi"); } } // extends 實現繼承 class Dog extends Animal { constructor(age) { super(age); //ES6 要求,子類的構造函數必須執行一次 super() 函數。 } } // extends 實現繼承 class Cat extends Animal { constructor(age) { super(age); } say() { super.say(); console.log("miao miao!!"); } } var cat = new Cat(11); var dog = new Dog(22); console.log(cat.age, dog.age); // 輸出11 22 繼承了父類的屬性 cat.say(); // 輸出 hi dog.say(); // 輸出 miao miao!!, hi

浙公網安備 33010602011771號