本文首發自騰訊IMWEB社區:imweb.io/javascript
由於我在學校接觸的第一門語言是cpp,是一個靜態類型語言,而且實現面向對象直接就有class關鍵字,並且只講了面向對象一種設計思想,致使我一直很難理解javascript語言的繼承機制。java
JavaScript沒有」子類「和」父類「的概念,也沒有」類「(class)和」實例「(instance)的區分,全靠」原型鏈「(prototype chain)實現繼承。c++
學的時候就很想吐槽,費了這麼大的勁去模擬類,那js幹嗎不一開始就設計class關鍵字而是最開始僅將class做爲保留字呢?(ES6以後有了class關鍵字,是原型的語法糖)es6
當時我一直懷疑,「js沒有class是一種設計缺陷嗎?」web
原來,JavaScript設計之初,設計裏面全部的數據類型都是對象(object),最開始,JavaScript只想被設計成一種簡易的腳本語言,設計者JavaScript裏面都是對象,必需要有一種機制將全部對象聯繫起來,但若是引入「類」(class)的概念,那麼就太「正式」了,增長了上手難度。編程
要實現繼承,但又不想用類,那該怎麼辦呢?bash
JavaScript 的設計者Brendan Eich發現,能夠像c++和Java語言中使用new命令生成實例。數據結構
因而new命令被引入到JavaScript,用來從原型對象生成一個實例對象。可是JavaScript沒有「類」,原型對象該如何表示呢?編程語言
這時,他想到c++和java使用new命令時,都會調用「類」的構造函數(constructor),因而他作了個簡化設計,在JavaScript中,new命令後面跟的不是類而是構造函數。函數
用構造函數生成實例對象,有一個缺點就是沒法共享屬性和方法。
每個實例對象,都有本身的屬性和方法的副本。這不只沒法作到數據共享,也是極大的資源浪費。
考慮到這一點,brendan Eich決定爲構造函數設置一個prototype屬性。
這個屬性包含一個prototype對象(是的,prototype屬性的值是prototype對象),全部的實例對象須要共享的屬性和方法,都放在這個對象裏面,那些不須要共享的屬性和方法,就放在構造函數裏。
實例對象一旦建立,將自動引用prototype對象的屬性和方法,也就是說,實例對象的屬性和方法,分紅兩種,一種是本地的,另外一種是引用的。
因爲全部的實例對象共享同一個prototype對象,那麼從外界看起來,prototype對象就好像是實例對象的原型,而實例對象則好像"繼承"了prototype對象同樣。
若是沒了解過c++、java或者其餘的編程語言,我相信你看完上面這段內容應該會看睡着了吧!好的,咱們仍是直接來看看代碼吧~
//原型鏈繼承
// 父類
// 擁有屬性 name
function parents(){
this.name = "JoseyDong";
}
// 在父類的原型對象上添加一個getName方法
parents.prototype.getName = function(){
console.log(this.name);
}
//子類
function child(){
}
//子類的原型對象 指向 父類的實例對象
child.prototype = new parents()
// 建立一個子類的實例對象,若是它有父類的屬性和方法,那麼就證實繼承實現了
let child1 = new child();
child1.getName(); // => JoseyDong
複製代碼
在只有一個 子類實例對象的時候,咱們貌似看不出什麼問題。然而在實際場景中,咱們會建立不少實例對象來繼承父類,畢竟繼承得越多,被複寫的代碼量就越多嘛~
//原型鏈繼承
// 父類
// 擁有屬性 name
function parents(){
this.name = ["JoseyDong"];
}
// 在父類的原型對象上添加一個getName方法
parents.prototype.getName = function(){
console.log(this.name);
}
//子類
function child(){
}
//子類的原型對象 指向 父類的實例對象
child.prototype = new parents()
// 建立一個子類的實例對象,若是它有父類的屬性和方法,那麼就證實繼承實現了
let child1 = new child();
child1.getName(); // => ["JoseyDong"]
// 建立一個子類的實例對象,在child1修改name前實現繼承
let child2 = new child();
// 修改子類的實例對象child1的name屬性
child1.name.push("xixi");
// 建立子類的另外一個實例對象,在child1修改name後實現繼承
let child3 = new child();
child1.getName();// => ["JoseyDong", "xixi"]
child2.getName();// => ["JoseyDong", "xixi"]
child3.getName();// => ["JoseyDong", "xixi"]
複製代碼
當不少時候,咱們的實例對象裏的值是會雖具體場景而改變的。好比這個時候,咱們的child1除了joseydong之外,她的朋友又給她取了個新名字xixi,咱們改變了child1的name值。而child一、child二、child3是三個獨立的個體,可是最後發現三個孩子都有了新名字!
這就表示,原型鏈繼承裏面,使用的都是同一個內存裏的值,這樣修改該內存裏的值,其餘繼承的子類實例裏的值都會變化。
這可不是咱們想要的效果,畢竟只有child1被賦予了新名字。而且,若是我想經過子類實例對象傳遞參數給父類,也是作不到的。
// 構造函數繼承
function parents(){
this.name = ["JoseyDong"];
}
// 在子類中,使用call方法構造函數,實現繼承
function child(){
parents.call(this);
}
let child1 = new child();
let child2 = new child();
child1.name.push("xixi");
let child3 = new child();
console.log(child1.name);// => ["JoseyDong", "xixi"]
console.log(child2.name);// => ["JoseyDong"]
console.log(child3.name);// => ["JoseyDong"]
複製代碼
咱們使用構造函數的方法,就只修改了child1的名字,而child2和child3的name屬性並無受影響~
同時,因爲call()支持傳遞參數,咱們也能夠在child中向parent傳參啦~
// 構造函數實現繼承
//子類向父類傳參
function parents(name){
this.name = name;
}
//call方法支持傳遞參數
function child(name){
parents.call(this,name)
}
let child1 = new child("I am child1");
let child2 = new child("I am child2");
console.log(child1.name);// => I am child1
console.log(child2.name);// => I am child2
複製代碼
好了,如今咱們經過構造函數實現繼承彌補了用原型鏈實現繼承的缺點,同時也是經過構造函數實現繼承的優勢:
1.避免了引用類型的屬性被全部實例共享
2.能夠在child中向parent傳參
可是,這種方式也有缺點,由於方法都在構造函數中定義,每次建立實例都會建立一遍方法。
咱們發現,經過原型鏈實現的繼承,都是複用同一個屬性和方法;經過構造函數實現的繼承,都是獨立的屬性和方法。因而咱們大打算利用這一點,將兩種方式組合起來:經過在原型上定義方法實現對函數的複用,經過構造函數的方式保證每一個實例都有它本身的屬性。
下面我再舉個栗子,讓你們感覺下組合繼承的好處~
//組合繼承
// 偶像練習生大賽開始報名了
// 初賽,咱們找了一類練習生
// 這類練習生都有名字這個屬性,但名字的值不一樣,而且都有愛好,而愛好是相同的
// 只有會唱跳rap的練習生纔可進入初賽
function student(name){
this.name = name;
this.hobbies = ["sing","dance","rap"];
}
// 咱們在student那類裏面找到更特殊的一類進入複賽
// 固然,咱們已經知道初賽時有了name屬性了,而不一樣練習生名字的值不一樣,因此使用構造函數方法繼承
// 同時,咱們想再讓練習生們再介紹下本身的年齡,每一個子類還能夠本身新增屬性
// 固然啦,具體的名字年齡就由每一個練習生實例來定
// 類只告訴你,有這個屬性
function greatStudent(name,age){
student.call(this,name);
this.age = age;
}
// 而你們的愛好值都相同,這個時候用原型鏈繼承就好啦
// 每一個對象都有構造函數,原型對象也是對象,也有構造函數,這裏簡單的把構造函數理解爲誰的構造函數就要指向誰
// 第一句將子類的原型對象指向父類的實例對象時,同時也把子類的構造函數指向了父類
// 咱們須要手動的將子類原型對象的構造函數指回子類
greatStudent.prototype = new student();
greatStudent.prototype.constructor = greatStudent;
// 決賽 kunkun和假kunkun進入了決賽
let kunkun = new greatStudent('kunkun','18');
let fakekun = new greatStudent('fakekun','28');
// 有請兩位選手介紹下本身的屬性值
console.log(kunkun.name,kunkun.age,kunkun.hobbies) // => kunkun 18 ["sing", "dance", "rap"]
console.log(fakekun.name,fakekun.age,fakekun.hobbies) // => fakekunkun 28 ["sing", "dance", "rap"]
// 這個時候,kunkun選手說本身還有個隱藏技能是打籃球
kunkun.hobbies.push("basketball");
console.log(kunkun.name,kunkun.age,kunkun.hobbies) // => kunkun 18 ["sing", "dance", "rap", "basketball"]
console.log(fakekun.name,fakekun.age,fakekun.hobbies)// => fakekun 28 ["sing", "dance", "rap"]
// 咱們能夠看到,假kunkun並無抄襲到kunkun的打籃球技能
// 而且若是這個時候新來一位選手,從初賽複賽闖進來的一匹黑馬
// 能夠看到黑馬並無學習到kunkun的隱藏技能
let heima = new greatStudent('heima','20')
console.log(heima.name,heima.age,heima.hobbies) // => heima 20 ["sing", "dance", "rap"]
複製代碼
能夠看到,組合繼承避開了原型鏈繼承和構造函數繼承的缺點,結合了二者的優勢,成爲了javascript中最經常使用的繼承方式。
這種繼承的思想是將傳入的對象做爲建立的對象的原型。
function createObj(o){
function F(){};
F.prototype = o;
return new F();
}
複製代碼
咱們來實現下原型式繼承,看看會不會有什麼問題
// 原型式繼承
function createObj(o){
function F(){};
F.prototype = o;
return new F();
}
let person = {
name:'JoseyDong',
hobbies:['sing','dance','rap']
}
let person1 = createObj(person);
let person2 = createObj(person);
console.log(person1.name,person1.hobbies) // => JoseyDong ["sing", "dance", "rap"]
console.log(person2.name,person2.hobbies) // => JoseyDong ["sing", "dance", "rap"]
person1.name = "xixi";
person1.hobbies.push("basketball");
console.log(person1.name,person1.hobbies) //xixi ["sing", "dance", "rap", "basketball"]
console.log(person2.name,person2.hobbies) //JoseyDong ["sing", "dance", "rap", "basketball"]
複製代碼
這個時候咱們發現,修改了person1的hobbies的值,person2的hobbies的值也變了。
這是由於包含引用類型的屬性值始終會共享相應的值,這點跟原型鏈繼承同樣~
而修改了person1.name的值,person2.name的值並未發生改變,並非由於person1和person2有獨立的name值,而是由於person1.name = "xixi"這條語句是給person1實例對象添加了一個name屬性,而它的原型對象上name值並無被修改,因此person2的name沒有變化。由於咱們找對象上的屬性時,老是先找實例對象,沒有找到的話再找原型對象上的屬性。實例對象和原型對象上若是有同名屬性,老是先取實例對象上的值。
ESMAScript5新增了Object.create()方法規範化了原型式繼承~
建立一個僅用於封裝繼承過程的函數,該函數在內部以某種形式來作加強對象,最後返回對象。
//寄生式繼承
function createObj(o){
let clone = Object.create(o);
clone.sayName = function(){
console.log('hi');
}
return clone
}
let person = {
name:"JoseyDong",
hobbies:["sing","dance","rap"]
}
let anotherPerson = createObj(person);
anotherPerson.sayName(); // => hi
複製代碼
固然,用寄生式繼承來爲對象添加函數,和借用構造函數模式同樣,每次建立對象都會建立一遍方法。
前面咱們說了,組合繼承是javascript最經常使用的繼承模式。這裏咱們先來回顧下組合式繼承的代碼:
//組合繼承
function student(name){
this.name = name;
this.hobbies = ["sing","dance","rap"];
}
function greatStudent(name,age){
student.call(this,name);
this.age = age;
}
greatStudent.prototype = new student();
greatStudent.prototype.constructor = greatStudent;
let kunkun = new greatStudent('kunkun','18');
複製代碼
組合繼承最大的缺點是最調用兩次父構造函數
一次是設置子類實例的原型的時候:
greatStudent.prototype = new student();
複製代碼
一次是在建立子類型實例的時候:
let kunkun = new greatStudent('kunkun','18');
複製代碼
在這個例子中,若是咱們打印一下kunkun這個對象,咱們就會發現greatStudent.prototype和kunkun都有一個屬性爲hobbies。
這其實就是實例對象和原型對象上的屬性值重複了,而再找屬性值的時候,在實例對象上找到了屬性值就不會在原型對象上找了,而這部分原型對象上的值就實打實的浪費了存儲空間。
那麼咱們該如何精益求精,避免這一次重複調用呢?
若是咱們不使用greatStudent.prototype = new student(),而是直接讓greatStudent.prototype訪問到student.prototype呢?
看看如何實現:
// 寄生組合式繼承
function student(name){
this.name = name;
this.hobbies = ["sing","dance","rap"];
}
function greatStudent(name,age){
student.call(this,name);
this.age = age;
}
//關鍵的三步 實現繼承
// 使用F空函數當子類和父類的媒介 是爲了防止修改子類的原型對象影響到父類的原型對象
let F = function(){};
F.prototype = student.prototype;
greatStudent.prototype = new F();
let kunkun = new greatStudent('kunkun','18');
console.log(kunkun);
複製代碼
打印結果:
能夠看到,kunkun實例的原型對象上再也不有hobbies屬性了。
最後,咱們封裝下這個繼承方法:
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
function prototype(child, parent) {
let prototype = object(parent.prototype);
prototype.constructor = child;
child.prototype = prototype;
}
// 當咱們使用的時候:
prototype(Child, Parent);
複製代碼
引用《JavaScript高級程序設計》中對寄生組合式繼承的誇讚就是:
這種方式的高效率體現它只調用了一次 Parent 構造函數,而且所以避免了在 Parent.prototype 上面建立沒必要要的、多餘的屬性。與此同時,原型鏈還能保持不變;所以,還可以正常使用 instanceof 和 isPrototypeOf。開發人員廣泛認爲寄生組合式繼承是引用類型最理想的繼承範式。
總而言之就是,這種js實現繼承的方式是最佳的。
然而,ES6以後經過extends關鍵字實現了繼承。
// ES6
class parents {
constructor(){
this.grandmather = 'rose';
this.grandfather = 'jack';
}
}
class children extends parents{
constructor(mather,father){
//super 關鍵字,它在這裏表示父類的構造函數,用來新建父類的 this 對象。
super();
this.mather = mather;
this.father = father;
}
}
let child = new children('mama','baba');
console.log(child) // =>
// father: "baba"
// grandfather: "jack"
// grandmather: "rose"
// mather: "mama"
複製代碼
子類必須在 constructor 方法中調用 super方法,不然新建實例時會報錯。這是由於子類沒有本身的this 對象,而是繼承父類的 this 對象,而後對其進行加工。
只有調用 super 以後,纔可使用 this 關鍵字,不然會報錯。這是由於子類實例的構建,是基於對父類實例加工,只有 super 方法才能返回父類實例。
ES5 的繼承實質是先創造子類的實例對象 this,而後再將父類的方法添加到 this 上面(Parent.call(this))。
ES6 的繼承機制實質是先創造父類的實例對象 this (因此必須先調用 super() 方法),而後再用子類的構造函數修改 this。
es6實現繼承的核心代碼以下:
function _inherits(subType, superType) {
subType.prototype = Object.create(superType && superType.prototype, {
constructor: {
value: subType,
enumerable: false,
writable: true,
configurable: true
}
});
if (superType) {
Object.setPrototypeOf
? Object.setPrototypeOf(subType, superType)
: subType.__proto__ = superType;
}
}
複製代碼
子類的 proto 屬性:表示構造函數的繼承,老是指向父類。 子類 prototype 屬性的 proto 屬性:表示方法的繼承,老是指向父類的 prototype 屬性。
除此以外,ES6 能夠自定義原生數據結構(好比Array、String等)的子類,這是 ES5 沒法作到的。