深刻淺出JavaScript繼承

寫在前面

若是對繼承尚未了解,或者瞭解不深的同窗,但願本文可以對你有所幫助,在閱讀過程當中有任何疑問或者意見,歡迎留言交流前端

父類函數約定

// 先約定好一個超類,即咱們要去繼承的父類
    // 雖然沒有強行規定,可是默認約定構造函數名採用大駝峯命名規則
    function Super (firstName) { 
        // 定一個實例屬性
        this.firstName = firstName;
    }
    // 寫一個原型屬性,方便後面講解
    Super.prototype.lastName = function (lastName) {
        return lastName;
    }
    
    /* 爲何上面的原型屬性lastName不寫成以下方式 Super.prototype = { lastName: function (lastName) { return lastName; } } 這兩種寫法有什麼不同? 能夠先思考一下 */
複製代碼

使用 call || apply 方法繼承

function Sub (firstName) {
    Super.call(this, firstName);
}

var aSub = new Sub('張');

console.log(aSub.firstName); // 趙
console.log(aSub.lastName('三')); //Uncaught TypeError: aSub.lastName is not a function
複製代碼

爲何會報錯

能夠看到在輸出aSub.lastName('三')的時候,程序報錯了,先不慌,咱們先打印一下aSub看看包含了什麼 數組

aSub
很明顯父類Super中的原型上的屬性是沒有繼承到的,這是爲何呢?究其緣由仍是call方法的原理,他只是循環拷貝了父類中的全部實例屬性,並無拷貝原型屬性

實例 aSub 的構造函數是誰?

console.log(aSub instanceof Sub) // true;
console.log(aSub instanceof Super) // false;
複製代碼

仍是 call 方法,他並無牽涉到原型指向的改變,因此對於使用call方法來講,並不會改變實例對象的指向的; 該誰 new 出來的誰就領走bash


吃瓜羣衆小明: instanceof是什麼鬼?
吃瓜羣衆大黃:instanceof運算符用來判斷一個構造函數的prototype屬性所指向的對象是否存在另一個要檢測對象的原型鏈上
吃瓜羣衆小明:說人話
吃瓜羣衆大黃:判斷孩子是否是他爹的
吃瓜羣衆小明: 這...微信


是否能多繼承

確定是能實現多繼承的,我說了不算,看下面代碼app

// 再添加一個父類
function Super2 (lastName){
    this.lastName = lastName;
}

// ok 看下面子類函數

function Sub (firstName, lastName) {
    Super.call(this, firstName);
    Super2.call(this, lastName);
}

var bSub =  new Sub('張', '三');

console.log(bSub.firstName,bSub.lastName); // 張三

複製代碼

好了好了,代碼已經告訴咱們了,call 是能夠實現多繼承的,可是注意一點,若是多個父類函數存在一樣的實例屬性,那麼後面繼承的父類中的實例屬性會覆蓋前面繼承的父類中的實例屬性;如上例中的Super2會覆蓋Super中的實例屬性,前提是實例屬性名字同樣,不同就各自安好函數


吃瓜羣衆小明: 什麼是多繼承?
吃瓜羣衆大黃:一個子類函數能夠繼承多個父類函數
吃瓜羣衆小明:說人話
吃瓜羣衆大黃:一個單純的女孩子能不能有多個乾爹
吃瓜羣衆小明: 這...
性能


父類中的實例屬性對於全部子類來講是不是獨立的


吃瓜羣衆小明: 能解釋一下上面的題目啥意思不?
吃瓜羣衆大黃:這不夠清楚?
吃瓜羣衆小明:能說人話不?
吃瓜羣衆大黃:爸爸有好幾個孩子,要給孩子一百錢用,嗯,話沒說清楚,這一百塊錢是多個孩子共一百,仍是每一個孩子都有一百呢?
吃瓜羣衆小明: 這...
學習


算了,仍是看代碼吧ui

// 咱們爲父類添加一個數組吧
function Super(firstName){
    this.firstName = firstName;
    this.like = [];
}


function SubA (firstName){
    Super.call(this);
}

function SubB (firstName){
    Super.call(this);
}

var aSubA = new SubA();
var aSubB = new SubB();

aSubA.like.push('music');

console.log(aSubA.like, aSubB.like) // ['music'] []

複製代碼

其實說到底仍是call方法,他將父類的實例屬性拷貝到了本身這裏,這就是他本身的了,私有的了,不和他人共用this

總結

綜上所述,call 之類方法繼承存在如下特色:

優勢:

  • 簡單,地球人都會寫
  • 能夠實現多繼承
  • 能夠在調用的時候自由傳參(路人甲:這算什麼優勢? 路人乙:滾,哎)

缺點:

  • 不能繼承原型上的屬性

中性:

  • 子類繼承而來的實例屬性都是獨立的
  • 實例對象指向的構造函數沒有發生改變,誰new的仍是指向誰,其實主要是原型指向沒有發現改變

原型繼承

function Sub(firstName, lastName) {}

Sub.prototype = new Super();

var aSub = new Sub('張');

console.log(aSub.firstName) // undefined 
複製代碼

能夠看到,在實例化的時候穿的參數好像並無起做用,那換種寫法吧

function Sub(firstName, lastName) {}

Sub.prototype = new Super('張');

var aSub = new Sub();

console.log(aSub.firstName) // 張
複製代碼

這下有用了,說明原型繼承在子類實例化的時候傳參並不能起做用,而是在父類實例化賦值給子類原型的時候纔有做用,接着往下看,這裏就體現了自由傳參是多麼幸福的一件事

function Super (firstName, lastName) {
    this.firstName = firstName;
    this.like = [];
}

function aSub(firstName, lastName) {}
function bSub(firstName, lastName) {}

aSub.prototype = new Super();
bSub.prototype = new Super();

var aSubA = new aSub();
var bSubB = new aSub();

aSubA.like.push('music');

console.log(aSubA.like, bSubB.like) // ["music"] ["music"]

複製代碼

能夠看到咱們只是在aSubA的like屬性添加了music,可是在bSubB.like中也有,這說明了他們是共用一個屬性的,再往下看

console.log(aSubA instanceof Super) // true
console.log(aSubA instanceof aSub) // true
複製代碼

ok,這能夠知道實例對象的構造函數指向是不明確的,主要的緣由是由於原型繼承的時候,aSub.prototype 發生了改變,指向了Super,咱們打印一下aSubA

很明顯,aSubA在原型鏈中查找aSub的時候,首先查找到了本身的構造函數aSub,那麼aSubA instanceof aSub 確定是成立的
而後aSubA在原型鏈中查找Super的時候,會經過原型鏈繼續向上查找aSub.__proto__發現指向了Super,因此aSubA instanceof Super 也是存在的,這就是爲何上面兩個判斷都是true

因此在這裏給個小建議,每次改變構造函數原型的時候,固然是直接賦值給構造函數原型的時候,建議手動修改constructor屬性的指向,修改成他本身的構造函數;這樣會在某些時刻不會由於constructor指向而產生沒必要要的麻煩;

好比上面 增長一條 aSubA.constructor = aSub;

還記得文章開頭的問題嗎?
Super.prototype.lastName = function () {} 與
Super.prototype = {lastName:function () {}}的區別;

Super.prototype.lastName 是在Super.prototype對象增長一個屬性;
而Super.prototype = {}這種方式是重寫了Super.prototype對象

咱們重寫的Super.prototype對象是沒有constructor的屬性的,這裏也建議加上constructor屬性,且添加值爲該構造函數;固然,具體的根據實際狀況而定

原型繼承的特色

優勢

  • 簡單快速高效
  • 繼承了父類中全部的屬性,包括原型屬性和實例屬性

缺點

  • 不能自由傳參,只能在父類函數構造賦值給子類函數原型的時候才能傳參
  • 不能實現多繼承,由於後面賦值覆蓋前面的賦值

中性

  • 全部子類函數公用一套屬性
  • 因爲改變了原型指向,致使了實例對象指向的構造函數不明

組合繼承

簡單說就是call繼承和原型繼承的結合,看實例

function Sub(firstName, lastName){
    Super.call(this,firstName, lastName) // 第二次
}
Sub.prototype = new Super(); // 第一次

var aSub = new Sub('張');

console.log(aSub.firstName, aSub.lastName('三')); // 張 三

複製代碼

固然既然是二者的結合,確定繼承了兩個的全部優勢,可是卻帶來了另外一個問題,那就是實例化屬性會重複賦兩遍值,固然會面的會覆蓋前面的,如上函數,第一次是在new Super()實例化並賦值的時候,會將全部的實例屬性給到Sub,第二次是Sub實例化的時候,會調用call方法從新將實例屬性在賦一遍,固然這問題不大,只是性能上來講多餘了,而這也是當前使用最多的繼承方式

組合繼承的特色

優勢

  • 繼承了call與原型繼承的全部優勢

缺點

  • 實例屬性重複賦值,即賦值了兩遍

中性

  • 子類繼承而來的實例屬性都是獨立的
  • 因爲改變了原型指向,致使了實例對象指向的構造函數不明

實例繼承

先看代碼

function Sub(firstName, lastName) {
    var backFun = new Super(firstName, lastName);
    return backFun;
}

var aSub = new Sub();
複製代碼

其實這裏就是取了一個巧,子類函數裏面,實例化父類函數,並將父類函數返回去; 因此確定父類全部的全部屬性他都會有

對於實例繼承來講,不論是將Sub做爲一個構造函數仍是普通函數,若是不添加新的屬性,是毫無區別的,由於最終執行的都會是父類的構造函數new Super; new Sub() === new Super();

若是不須要改變父類實例屬性的值,你甚至均可以直接這樣寫

function Sub(firstName, lastName) {
    return new Super(firstName, lastName);
}

var aSub = new Sub() ||  Sub();
複製代碼

實例繼承的特色

優勢

  • 書寫簡單,容易理解
  • 完美的繼承父類全部的屬性

缺點

  • 沒法完成多繼承,主要由於return 只能返回一個值,除非你是用對象的方式來寫,可是這樣可讀性就差了,並且繼承而來的屬性須要處理

特色

  • 父類的就是子類的,固然這裏的子類實例對象是不屬於子類構造函數的,由於其實執行子類構造函數就是執行了父類構造函數

能夠動手試試 aSub instanceof Sub 和 aSub instanceof Super

對象冒充繼承

仍是看代碼

function Sub(firstName, lastName) {
    this.methods = Super; // 將函數複製給實例屬性methods
    this.methods(firstName, lastName); // 執行函數
    delete this.methods; // 刪除臨時建立多餘的實例屬性
}

var aSub = new Sub('張') ;

console.log(aSub.firstName)
複製代碼

this.methods()執行的時候,methods裏面的this指向的是Sub,因此js會將Super中的全部this下面的屬性所有提取到Sub對象中,可能我猜這也是爲何這種繼承被稱爲冒充繼承的緣由了,將父類做爲本身的一個實例屬性,在執行的過程當中,將父類的屬性冒充成本身的屬性,嗯,這很是流氓0.0,不信,那咱們打印一下aSub吧

這以前咱們作一個小小的改變,是爲了看的更清楚

function Sub(firstName, lastName) {
    this.methods = Super; // 將函數複製給實例屬性methods
    this.methods(firstName, lastName); // 執行函數
}

var aSub = new Sub() ;

console.log(aSub)
複製代碼

aSub

看到沒有,在methods屬性中沒有任何任何屬性,而屬性firstName卻跑到了Sub對象下面

冒充繼承的特色和call方法的特色差很少,能夠參考call繼承特色

寄生組合繼承

看名字就知道這種方式實現起來可能會有點繁雜了,其實只是組合繼承的一種變異而已,主要是爲了解決父類實例屬性屢次賦值的問題,好了直接看代碼吧

function Sub(firstName){
    Super.call(this, firstName) 
}

function methods() { 
    /* 主要步驟 爲何寫成一個函數?主要是體現這一步的重要性, 還有就是method方法只會在這裏使用,不必暴露出去 */
    var method = function () {};
    method.prototype = Super.prototype;
    Sub.prototype = new method();
    Sub.prototype.contructor = Sub;
}
methods();

var aSub = new Sub();

複製代碼

在methods方法中,主要是利用了原型鏈查找的特色,咱們先定一個空的函數表達式method;而後將他的原型指向父類的原型,最後將子類的原型指向空函數method的實例化;

看這一步

function methods() { // 主要步驟
    var method = function () {};
    method.prototype = Super.prototype;
    Sub.prototype = new method();
    Sub.prototype.contructor = Sub;
}
methods();

複製代碼

這一步能簡化爲 Sub.prototype = Super.prototype嗎? 這一步的目的不就是爲了讓sub可以與繼承Super的原型屬性嗎?

道理是這個道理,若是不考慮原型屬性共用的話,徹底能夠直接Sub.prototype = Super.prototype,可是,這樣寫了,原型屬性就所有共用了,子類A改變了了原型屬性值 的話,那麼子類B也會發生改變,由於都是共用的父類的原型屬性,

若是不想共用原型屬性的話,能夠像如上處理,由於Sub.prototype = new method() 這一步,不一樣的子類都建立了一個新的new method(); 這裏能夠想一想一下函數的執行,每次執行都會建立一個新的OA,因此確定都是相互獨立的,不會互相影響

Object.create你們認識一下,這個方法實現很簡單,看下面代碼

Object.create = function (objProto) {
    var Fun = function() {};
    Fun.prototype = objProto;
    return new Fun();
}
複製代碼

是否是感受有點眼熟,是的,很大一部分都和咱們上面的method方法差很少,因而咱們進行改造

function Sub(firstName){
    Super.call(this, firstName) 
}

Sub.prototype = Object.create(Super.prototype);

var aSub = new Sub();

複製代碼

ok 簡化多了

寄生組合方法比較完美的解決了各類繼承所帶來的問題,可是確實須要你們多原型及原型鏈有必定的認識

繼承的方式主要是看你對原型的理解程度,因此說原型纔是咱們真正要完全征服的那個對象

寫在最後

這篇文章感受在草稿裏躺了很久了,這段時間一直忙,也沒時間來寫博客,因此趁着這段時間稍微閒一些,趕忙完善一下發出去了

若是喜歡的能夠點個贊0.0

仍是那句話,若是本文有誤的地方歡迎指正,或者有什麼建議的也能夠留言交流,固然,謝絕無腦噴,文明上網,社會和諧,哈哈哈

最後:歡迎你們關注個人微信公衆號:大前端js,固然爲了回饋你們關注,裏面我放了一些學習資源,熱烈歡迎你們關注交流前端方面但不侷限前端方面的知識;

相關文章
相關標籤/搜索