前言 很久以前學習《Javascript語言精粹》時,寫過一個關於js的系列學習筆記。 最近又跟別人講什麼原型和繼承什麼的,發現這些記憶有些模糊了,然後回頭看自己這篇文章,覺得幾年前的學習筆記真是簡陋。 所以在這裡將這篇繼承重新更新一下,並且加上ES6的部分,以便下次又對這些記憶模糊了,能憑藉這篇文 ...
前言
很久以前學習《Javascript語言精粹》時,寫過一個關於js的系列學習筆記。
最近又跟別人講什麼原型和繼承什麼的,發現這些記憶有些模糊了,然後回頭看自己這篇文章,覺得幾年前的學習筆記真是簡陋。
所以在這裡將這篇繼承重新更新一下,並且加上ES6的部分,以便下次又對這些記憶模糊了,能憑藉這篇文章快速回憶起來。
本篇文章關於ES5的繼承方面參考了《Javascript語言精粹》和《JS高程》,後面的ES6部分通過使用Babel轉換為ES5代碼,然後進行分析。
用構造函數聲明對象
既然講到繼承,那麼有必要先說一下對象是如何通過構造器構造的。
先看以下代碼:
function Parent(){
this.type='人'
this.name='爸爸';
this.body={
weight:50
}
}
當聲明瞭Parent這個構造函數後,Parent會有一個屬性prototype,這是Parent的原型對象。
可以相當於有以下這段隱式代碼
Parent.prototype={constructor:Parent}
執行構造函數構造對象時,會先根據原型對象創造一個對象A,然後調用構造函數,通過this將屬性和方法綁定到這個對象A上,如果構造函數中不返回一個對象,那麼就返回這個對象A。
原型鏈
然後可以看下js最初的繼承。
以下為最基本的原型鏈玩法
function Parent(){
this.type='人'
this.name='爸爸';
this.body={
weight:50
}
}
function Child(){
this.name='兒子';
}
Child.prototype=new Parent();
var child=new Child();
console.info(child.name);//兒子
console.info(child.type);//人
通過以上發現,我們的child繼承了原型對象的type。
借用構造函數:保證父級引用對象屬性的獨立性
原型鏈最大的問題在於,當繼承了引用類型的屬性時,子類構造的對象就會共用父類原型對象的引用屬性。
我們先來看之前的例子,child不僅繼承了parent的type,也繼承了body。
修改一下以上的代碼:
var child=new Child();
child.body.weight=30;
var man=new Child();
console.info(man.type);//30
這裡可以看到,原型中的引用對象被child和man共用了。
子類構造函數構造child和man兩個對象。
當他們讀取父級屬性時,讀取的是同一個變數地址。
如果在子級對象中更改這些屬性的值,那麼就會在子級對象中重新分配一個地址寫入新的值,那麼就不存在共用了屬性。
但是上面的例子中,是更改引用對象body里的值weight,而不是body。
這樣的結果就是body的變數地址不變,導致父級引用對象被子級對象共用,失去了各個子級對象應該有的獨立性(這裡我只能用獨立性來說明,為了避免和後面講到的私有變數弄混)。
於是就有了借用構造函數的玩法:
function Parent(){
this.type='人'
this.name='爸爸';
this.body={
weight:50
}
}
function Child(){
Parent.call(this);
this.name='兒子';
}
Child.prototype=new Parent();
var child=new Child();
console.info(child.name);//兒子
console.info(child.type);//人
實際上借用構造函數是在在子類的構造函數中借用父類的構造函數,然後就在子類中把所有父類的屬性都聲明瞭一次。
然後在子類構造對象後,獲取屬性時,因為子對象已經有了這個屬性,那麼就不會去查找原型鏈上的父對象的屬性了,從而保證了繼承時父類中引用對象的獨立性。
組合繼承:函數的復用
組合嘛,實際上就是利用了原型鏈來處理一些需要共用的屬性或者方法(通常是函數),以達到復用的目的,又借用父級的構造函數,來實現屬性的獨立性
在上面的代碼中加入
Parent.prototype.eat = function(){
// 吃飯
}
這樣Child構造的對象就可以繼承到eat這個函數
原型繼承:道格拉斯的原型式繼承
這個方式是道格拉斯提出來的,也就是寫《JavaScript語言精粹》的那個人。
實際上就是Object.create。
它的最初代碼如下:
Object.create=function(origin){
function F(){};
F.protoType=origin;
return new F();
}
var child=Object.create(parent);
這個玩法的思路就是以後我們不要用js的那種偽類寫法了,擺脫掉類這個概念,而是用對象去繼承對象,也就是原型繼承,因為相對於那些基於類的語言,js有自己進行代碼重用的方式。
但是這個樣子依然會有問題,就是在原型繼承中,同一個父對象的不同子對象共用了繼承自父對象的引用類型。
導致的結果就是一個子對象的值發生了改變,另外一個也就變了。
也就是我們說的引用屬性獨立性的問題。
寄生式繼承:增強原型繼承子對象
道格拉斯又提出了寄生式繼承:
function createObject(origin){
var clone=Object.create(origin);
clone.eat=function(){
// 吃飯
}
}
這種繼承你可以看做毫無意義,因為你一般會寫成下麵這樣:
var clone=Object.create(origin);
clone.eat=function(){
// 吃飯
}
這個樣子寫也沒毛病。
當然你可以認為這是一次重構,從提煉一個業務函數的角度去理解就沒毛病了。比如通過人這個原型創造男人這個對象。
寄生組合式繼承:保證原型繼承中父級引用對象屬性的獨立性
組合繼承的問題在於,會兩次調用父級構造函數,第一次是創造子類型原型的時候,另一次是子類型構造函數內部去復用父類型構造函數。
對於一個大的構造函數而言,可能對性能產生影響。
而原型繼承以及衍生出的寄生式繼承的毛病就是,引用類型的獨立性有問題。
那麼堪稱完美的寄生組合式繼承就來了,但是在之前,我們先回顧下這段組合式繼承的代碼:
function Parent(){
this.type='人'
this.name='爸爸';
this.body={
weight:50
}
}
function Child(){
Parent.call(this);
this.name='兒子';
}
Parent.prototype.eat=function(){
//吃
}
Child.prototype=new Parent();
var child=new Child();
那麼現在我們加入寄生式繼承的修改:
function Parent(){
this.type='人'
this.name='爸爸';
this.body={
weight:50
}
}
function Child(){
Parent.call(this);
this.name='兒子';
}
Parent.prototype.eat=function(){
//吃
}
function inheritPrototype(childType,parentType){
var prototype=Object.create(Parent.prototype);
prototype.constructor=childType;
childType.prototype=prototype
}
inheritPrototype(Child,Parent)
var child=new Child();
或者我們把inheritPrototype寫得更容易懂一點:
function inheritPrototype(childType,parentType){
childType.prototype=Object.create(Parent.prototype);
childType.prototype.constructor=childType;
}
記住這裡不能直接寫成
childType.prototype=Parent.prototype
錶面上看起來可以,但是childType上原型加上函數,那麼父級就會加上,這樣不合理。
通過inheritPrototype,Child直接以Parent.prototype的原型為原型,而不是new Parent(),那麼也就不會繼承Parent自己的屬性,而又完美繼承了Parent原型上的eat方法。
通過借用構造函數又實現了引用屬性的獨立性。
那麼現在我們來看就比較完美了。
只不過這種方式我平常都基本不用的,因為麻煩,更喜歡一個Object.create解決問題,只要註意引用對象屬性的繼承這個坑點就行。
函數化:解決繼承中的私有屬性問題
以上所有生成的對象中都是沒有私有屬性和私有方法的,只要是對象中的屬性和方法都是可以訪問到的。
這裡為了做到私有屬性可以通過函數化的方法。
function Parent(){
this.type='人'
this.name='爸爸';
this.body={
weight:50
}
}
function Child(){
Parent.call(this);
this.name='兒子';
}
inheritPrototype(Child,Parent)
var ObjectFactory=function(parent){
var name='troy';//私有變數
result=new Child();
result.GetMyName=function(){//這是要創建的對象有的特有方法
return 'myname:'+name;
}
return result;
};
var boy=ObjectFactory(anotherObj);
這個地方實際上用的是閉包的方式來處理。
拷貝繼承
這裡其實還有一種複製屬性的玩法,繼承是通過複製父對象屬性到子對象中,但是這種玩法需要for in遍歷,如果要保持引用對象獨立性,還要進行遞歸遍歷。
這裡就不介紹了。
它有它的優點,簡單,避開了引用對象獨立性,並且避開了從原型鏈上尋找對象這個過程,調用屬性的時候更快,缺點是這個遍歷過程,對於屬性多層級深的對象用這種玩法,不是很好。
關於ES5繼承的一些說法
ES5一直都是有偽類繼承(也就是通過構造函數來實現繼承)和對象繼承(我認為原型和拷貝都算這種)兩種玩法,一種帶著基於類的思想的玩法,一種純粹從對象的角度去考慮這個事情。
如果加上類的概念的話,確實麻煩,如果是直接考慮原型繼承而不用考慮類的話就簡單很多。
所以對ES5而言可以更多從對象角度去考慮繼承,而不是從類的角度。
ES6:進化,class的出現
在ES6中出現了class的玩法,這種類的玩法使得我在使用ES6的時候更願意去站在類的角度去思考繼承,因為基於類去實現繼承更加簡單了。
新的class玩法,並沒有改變Javascript的通過原型鏈繼承的本質,它更像是語法糖,只是讓代碼寫起來更加簡單明瞭,更加像是一個面向對象語言。
class Parent {
constructor(name){
super()
this.name = name
}
eat(){
console.log('吃飯');
}
}
class Child extends Parent {
constructor(name,gameLevel){
super(name)
this.gameLevel = gameLevel
}
game(){
console.log(this.gameLevel);
}
}
我們可以通過Babel將它轉化為ES5:
'use strict';
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var Parent = function () {
function Parent(name) {
_classCallCheck(this, Parent);
this.name = name;
}
_createClass(Parent, [{
key: 'eat',
value: function eat() {
console.log('吃飯');
}
}]);
return Parent;
}();
var Child = function (_Parent) {
_inherits(Child, _Parent);
function Child(name, gameLevel) {
_classCallCheck(this, Child);
var _this = _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).call(this, name));
_this.gameLevel = gameLevel;
return _this;
}
_createClass(Child, [{
key: 'game',
value: function game() {
console.log(this.gameLevel);
}
}]);
return Child;
}(Parent);
然後在這裡我們不考慮相容性,提煉一下核心代碼,並美化代碼以便閱讀:
'use strict';
var _createClass = function () {
function defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key,descriptor);
}
}
return function (Constructor, protoProps, staticProps)
{
if (protoProps)
defineProperties(Constructor.prototype, protoProps);
if (staticProps)
defineProperties(Constructor, staticProps);
return Constructor;
};
}();
function _inherits(subClass, superClass) {
subClass.prototype = Object.create(superClass.prototype);
subClass.__proto__ = superClass;
}
var Parent = function () {
function Parent(name) {
this.name = name;
}
_createClass(Parent, [{
key: 'eat',
value: function eat() {
console.log('吃飯');
}
}]);
return Parent;
}();
var Child = function (_Parent) {
_inherits(Child, _Parent);
function Child(name, gameLevel) {
Child.__proto__.call(this, name);
var _this = this;
_this.gameLevel = gameLevel;
return _this;
}
_createClass(Child, [{
key: 'game',
value: function game() {
console.log(this.gameLevel);
}
}]);
return Child;
}(Parent);
在這裡可以看到轉化後的代碼很接近我們上面講述的寄生組合式繼承,在_inherits中通過Object.create讓子類原型繼承父類原型(這裡多了一步,Child.__proto__=Parent),而在Child函數中,通過Child.__proto__借用父級構造函數來構造子類對象。
而_createClass不過是把方法放在Child原型上,並把靜態變數放在Child上。
總結
總的來說ES6中,用class就好,但是要理解這個東西不過是ES5的寄生組合式繼承玩法的語法糖而已,而不是真的變成那種基於類的語言了。
如果有天還讓我寫ES5代碼,父類中沒有引用對象,那麼使用Object.create是最方便的,如果有,那麼可以根據實際情況考慮拷貝繼承和寄生組合式繼承。