2018.5.27
今天本人又在查關於繼承的問題,重新溫習了一遍書,發現之前舉的例子實際上不太清晰,故做調整。
我的上一篇文章介紹了,原型鏈繼承模式。原型鏈繼承雖然很強大,但是單純的原型鏈模式並不能很好地實現繼承。
一、原型鏈的缺點
1.1 單純的原型鏈繼承最大的一個缺點,在於對原型中引用類型值的誤修改。
先看一個例子:
//父類:人 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.prototype 的值為 Person 類的一個實例,實現了 Student 類對 Person 類的繼承。所以 ,stu1 能訪問到父類 Person 上定義的 head 屬性,打印值為“腦袋瓜子”。我們知道,所有的 Student 實例都共享着原型對象上的屬性。那么,如果我在 stu1 上改變了 head 屬性值,是不是會影響原型對象上的 head 值呢?看我上面的代碼就知道,肯定是不會。stu1 的 head 值確實是改變了,但是我重新實例化的對象 stu2 的 head 值仍舊不變。
這是因為,當實例中存在和原型對象上同名的屬性時,會自動屏蔽原型對象上的同名屬性。stu1.head = "聰明的腦袋瓜子" 實際上只是給 stu1 添加了一個本地屬性 head 並設置了相關值。所以當我們打印 stu1.head 時,訪問的是該實例的本地屬性,而不是其原型對象上的 head 屬性(它因和本地屬性名同名已經被屏蔽了)。
剛才我們討論的這個 head 屬性是一個基本類型的值,可如果它是一個引用類型呢?這其中又會有一堆小九九。
其實原型對象上任何類型的值,都不會被實例所重寫/覆蓋。在實例上設置與原型對象上同名屬性的值,只會在實例上創建一個同名的本地屬性。
但是,原型對象上引用類型的值可以通過實例進行修改,致使所有實例共享着的該引用類型的值也會隨之改變。
再看下面這個例子:
//父類:人 function Person () { this.head = '腦袋瓜子'; this.emotion = ['喜', '怒', '哀', '樂']; //人都有喜怒哀樂 } //子類:學生,繼承了“人”這個類 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); //["喜", "怒", "哀", "樂", "愁"]
我們在剛才的 Person 類中又添加了一個 emotion 情緒屬性,人都有喜怒哀樂嘛。尤其需要注意的是,這是一個引用類型的值。這時,stu1 認為他還很“愁”,所以就通過 stu1.emotion.push ( ) 方法在原來的基礎上增加了一項情緒,嗯,打印出來“喜怒哀樂愁”,沒毛病。可是 stu2 是個樂天派,他咋也跟着一起愁了呢?!肯定不對嘛~
這就是單純的原型鏈繼承的缺點,如果一個實例不小心修改了原型對象上引用類型的值,會導致其它實例也跟着受影響。
因此,我們得出結論,原型上任何類型的屬性值都不會通過實例被重寫,但是引用類型的屬性值會受到實例的影響而修改。
1.2 原型鏈不能實現子類向父類中傳參。這里就不細說了。
二、借用構造函數
2.1 實現原理
在解決原型對象中包含引用類型值所帶來問題的過程中,開發人員開始使用一種叫做借用構造函數的技術。實現原理是,在子類的構造函數中,通過 apply ( ) 或 call ( )的形式,調用父類構造函數,以實現繼承。
//父類:人 function Person () { this.head = '腦袋瓜子'; this.emotion = ['喜', '怒', '哀', '樂']; //人都有喜怒哀樂 } //子類:學生,繼承了“人”這個類 function Student(studentID) { this.studentID = studentID; Person.call(this); } //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); //["喜", "怒", "哀", "樂"]
細心的同學可能已經發現了,該例子與上面的例子非常相似,只是去掉了之前通過 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 屬性副本。他們互不影響。說到這里,大家應該清楚一點點了吧。
總之,在子類函數中,通過call ( ) 方法調用父類函數后,子類實例 stu1, 可以訪問到 Student 構造函數和 Person 構造函數里的所有屬性和方法。這樣就實現了子類向父類的繼承,而且還解決了原型對象上對引用類型值的誤修改操作。
2.2 缺點
這種形式的繼承,每個子類實例都會拷貝一份父類構造函數中的方法,作為實例自己的方法,比如 eat()。這樣做,有幾個缺點:
1. 每個實例都拷貝一份,占用內存大,尤其是方法過多的時候。(函數復用又無從談起了,本來我們用 prototype 就是解決復用問題的)
2. 方法都作為了實例自己的方法,當需求改變,要改動其中的一個方法時,之前所有的實例,他們的該方法都不能及時作出更新。只有后面的實例才能訪問到新方法。
//父類:人 function Person () { this.head = '腦袋瓜子'; this.emotion = ['喜', '怒', '哀', '樂']; //人都有喜怒哀樂 this.eat = function () { console.log('吃吃喝喝'); } this.sleep = function () { console.log('睡覺'); } this.run = function () { console.log('快跑'); } }
所以,無論是單獨使用原型鏈繼承還是借用構造函數繼承都有自己很大的缺點,最好的辦法是,將兩者結合一起使用,發揮各自的優勢。我將在下一篇文章作出解釋。js 繼承之組合繼承
如果你覺得文章解決了你的疑惑的話,還請賞我一個推薦哦~ :)
作者不才,文中若有錯誤,望請指正,避免誤人子弟。
文章內容全都參考於《JAVASCRIPT 高級程序設計》)