本文講述JavaScript中類繼承的實現方式,並比較實現方式的差別。javascript
繼承,是子類繼承父類的特徵和行爲,使得子類對象具備父類的實例域和方法。
繼承是面向對象編程中,不可或缺的一部分。java
減小代碼冗餘
父類能夠爲子類提供通用的屬性,而沒必要由於增長功能,而逐個修改子類的屬性代碼複用
同上代碼易於管理和擴展
子類在父類基礎上,能夠實現本身的獨特功能耦合度高
若是修改父類代碼,將影響全部繼承於它的子類影響性能
子類繼承於父類的數據成員,有些是沒有使用價值的。可是,在實例化的時候,已經分配了內存。因此,在必定程度上影響程序性能。例子以圖書館中的書入庫歸類爲例。
如下是簡化後的父類Book
(也可稱爲基類)。
目的是經過繼承該父類,產出Computer
(計算機)子類。
而且,子類擁有新方法say
,輸出本身的書名。es6
function Book(){ this.name = ''; // 書名 this.page = 0; // 頁數 this.classify = ''; // 類型 } Book.prototype = { constructor: Book, init: function(option){ this.name = option.name || ''; this.page = option.page || 0; this.classify = option.classify || ''; }, getName: function(){ console.log(this.name); }, getPage: function(){ console.log(this.page); }, getClassify: function(){ console.log(this.classify); } };
接下來會講解子類Computer
幾種繼承方式的實現和優化方法。開始飆車~編程
function Computer(){ Book.apply(this, arguments); } Computer.prototype = new Book(); Computer.prototype.constructor = Computer; Computer.prototype.init = function(option){ option.classify = 'computer'; Book.prototype.init.call(this, option); }; Computer.prototype.say = function(){ console.log('I\'m '+ this.name); }
function Computer(){ Book.apply(this, arguments); }
Computer
的構造函數裏,調用父類的構造函數進行初始化操做。使子類擁有父類同樣的初始化屬性。瀏覽器
Computer.prototype = new Book();
使用new操做符對父類Book
進行實例化,並將實例對象賦值給子類的prototype
。
這樣,子類Computer
就能夠經過原型鏈訪問到父類的屬性。app
Book
的構造函數被執行了2次
Computer
的構造函數裏Book.apply(this, arguments);
Computer.prototype = new Book();
Computer.prototype = new Book();
,這種實例化方式,沒法讓Book
父類接收不固定的參數集合。function Computer(){ Book.apply(this, arguments); } Computer.prototype = Object.create(Book.prototype); Computer.prototype.constructor = Computer; Computer.prototype.init = function(option){ option.classify = 'computer'; Book.prototype.init(option); }; Computer.prototype.say = function(){ console.log('I\'m '+ this.name); }
這裏的改進:是使用Object.create(Book.prototype)
。它的做用是返回一個繼承自原型對象Book.prototype
的新對象。且該對象下的屬性已經初始化。
用Object.create
生成新對象,並不會調用到Book
的構造函數。
這種方式,也能夠經過原型鏈實現繼承。函數
因爲低版本的瀏覽器是不支持Object.create
的。因此這裏簡單介紹下兼容版本:性能
Object.create = function(prototype){ function F(){} F.prototype = prototype; return new F(); }
原理是定義一個空的構造函數,而後修改其原型,使之成爲一個跳板,能夠將原型鏈傳遞到真正的prototype。優化
上述兩種實現方式,都存在一個問題:不存在私有屬性
和私有方法
。也就是說,存在被篡改的風險。
接下來就用函數化來化解這個問題。this
function book(spec, my){ var that = {}; // 私有變量 spec.name = spec.name || ''; // 書名 spec.page = spec.page || 0; // 頁數 spec.classify = spec.classify || ''; // 類型 var getName = function(){ console.log(spec.name); }; var getPage = function(){ console.log(spec.page); }; var getClassify = function(){ console.log(spec.classify); }; that.getName = getName; that.getPage = getPage; that.getClassify = getClassify; return that; } function computer(spec, my){ spec = spec || {}; spec.classify = 'computer'; var that = book(spec, my); var say = function(){ console.log('I\'m '+ spec.name); }; that.say = say; return that; } var Ninja = computer({name: 'JavaScript忍者祕籍', page: 350});
函數化的優點,就是能夠更好地進行封裝和信息隱藏。
也許有人疑惑爲何用如下這種方式聲明和暴露方法:
var say = function(){ console.log('I\'m '+ spec.name); }; that.say = say;
實際上是爲了保護對象自身的完整性。即便that.say
被外部篡改或破壞掉,function computer
內部的say
方法仍然可以正常工做。
另外,解釋下that
、spec
和my
的做用:
that
是一個公開數據存儲容器,暴露出去的數據接口,都放到這個容器spec
是用來存儲建立新實例所需的信息,屬於實例之間共同編輯的數據my
是用來存儲父類、子類之間共享的私密數據容器,外部是訪問不到的。最後,看下現代版ES6的類繼承。不由感慨之前的刀耕火種,是多麼折磨人🌚
class Book { constructor(){ this.name = ''; // 書名 this.page = 0; // 頁數 this.classify = ''; // 類型 } init(option) { this.name = option.name || ''; this.page = option.page || 0; this.classify = option.classify || ''; } getName() { console.log(this.name); } getPage (){ console.log(this.page); } getClassify (){ console.log(this.classify); } } class Computer extends Book{ constructor(...args){ super(...args); } init(option) { super.init(option); this.classify = 'computer'; } say() { console.log('I\'m '+ this.name); } }
雖然ES5終究會被淘汰,可是瞭解下其工做原理,仍是頗有必要。由於不少源碼仍是有用到裏面的模式。 附帶的價值就是,ES5的繼承玩到飛起,ES6的繼承就是小菜一碟。