Spirit帶你徹底搞懂JS的6種繼承方案

JavaScript中實現繼承的6種方案

01-原型鏈的繼承方案

function Person(){
    this.name="czx";
}
function Student(){}
var p1=new Person();
Student.prototype=p1;
var student1=new Student();
console.log(student1); // Person{}
console.log(student1.name); // czx

這是最簡單的一種方案,同時也是弊端最多的方案,我們來分析下他的弊端

  1. 如果直接列印Student的實例對象,列印出來是這樣

    image-20210928104131722

    為啥不是列印出來的Student呢,我們先得了解一個東西,列印出來的這個名稱是由constructor決定的

    我們直接將Person的實例對象賦值給Student的prototype,Student原來的prototype就被覆蓋了
    而Person的實例對象的隱式原型是指向Person的prototype

    而Person的prototype中是有constructor的,constructor是指向Person本身的
    你們可以這麼理解,Person.prototype,那麼constructor就是指向Person,Object.prototype中的constructor就是指向Object

    所以,Student1沒找到自己的constructor,沿著原型鏈往上找,找到了Person的constructor,所以就是Person

  2. 通過這種方式繼承過來,列印學生的實例對象,繼承過來的屬性是看不見的

  3. 如果在父類Person上添加引用類型的數據,如果我們創建多個學生對象實例,修改了引用類型的數據,那麼父類中的數據也是會被改變的,那麼實例的對象,相互之間就不是獨立的,可以看下下面這段程式碼

    function Person(){
        this.friends=[];
    }
    function Student(){}
    var p1=new Person();
    Student.prototype=p1;
    var student1=new Student();
    var student2=new Student();
    student1.friends.push("czx");
    console.log(student2.friends); // ["czx"]
    
  4. 無法傳遞參數

    細心的同學可能就會發現,我這樣寫是無法傳遞參數的.那以後我們要傳遞參數的話,就會相當麻煩

    🎈所以,接下來我們看第二種方式


02-借用構造函數實現繼承

這個方案的核心就是 call

function Person(name, age) {
    this.name = name;
    this.age = age;
}


// 第一個弊端: Person執行了兩次
// 第一次是new出來的,第二次是call執行的
// 第二個弊端是 在new的時候,會往student上加上一些不必要的屬性,會把繼承的屬性加到了Student上面,這是沒必要的
Student.prototype = new Person();
Student.prototype.studying = function () {
    console.log(this.name + "再學習");
}
function Student(name, age, sno) {
    Person.call(this, name, age);
    this.sno = sno;
}

let stu1 = new Student("czx", 18, 180047101);
stu1.studying();
console.log(stu1);

這個方案解決了第一種方案的(2),(3),(4)弊端

  1. 這兩個放的順序一定不能放錯了

    Student.prototype = new Person();
    Student.prototype.studying = function () {
        console.log(this.name + "再學習");
    }
    

    順序放錯的話 studying那個函數加在了Student原來的原型上,而後又把Person的實例對象覆蓋了Student的原型,那麼在我們調用的話,就找不到了

  2. 我們來看下這一段

    function Student(name, age, sno) {
        Person.call(this, name, age);
        this.sno = sno;
    }
    

    因為在我們內直接使用call調用了Person並執行了,那麼Person的屬性也都可以在Student裡面使用,這也能使的我們可以傳遞屬性,可以復用父類中定義的屬性.並且在我們列印的時候,也能把屬性全部列印出來,大家可以自己試試

  3. 這個方案說實話已經很不錯了,解決了大部分問題,如果簡單使用也是可以的,但是這種方案就沒有弊端嗎?
    答案是有的,我們現在來看下它的弊端

    1. 第一個弊端: Person執行了兩次
    2. 第一次是new出來的,第二次是call執行的
    3. 第二個弊端是 在new的時候,會往student上加上一些不必要的屬性,會把繼承的屬性加到了Student上面,這是沒必要的,我們只需要在call的時候,調用Person,並且把屬性放到Student上面去就行了
    4. 第一種方案的第一個弊端也沒有解決

🎆我們現在來看下一種方案


03-直接繼承父類的原型

function Person(name) {
    this.name = name;
}
Person.prototype.running = function () {
    console.log(this.name + " is running");
}

function Teacher(name, career) {
    Person.call(this, name);
    this.career = career;
}
function Student(name, age) {
    Person.call(this, name);;
    this.age = age;
}

// 這種方法可不可行呢?
// 答案是不行
// 我們看下面的例子
// 我明明是給學生單獨加的屬性,但是teacher也可以共用它,這是不合理的
// 我們需要的是學生和老師自己的原型上是屬於自己獨一無二的屬性
// 這樣子可復用性才會高
Student.prototype = Person.prototype;
Teacher.prototype = Person.prototype;
Student.prototype.studying = function () {
    console.log(this.name + " is learning");
}

var s1 = new Student("czx", 18);
var t1 = new Teacher("teacher", "math");
console.log(s1);
s1.running()
s1.studying();
t1.studying();

這種方案是基於第二種方案的一個改版,但是我們看下這種方案可不可行

看完程式碼,大家也知道不行了,我在程式碼上也有詳細的注釋

  1. 我明明是給學生單獨加的屬性,但是teacher也可以共用它,這是不合理的,這是因為我們加的方法都是直接加在了父類的原型上
  2. 我們需要的是學生和老師自己的原型上是屬於自己獨一無二的屬性
  3. 這樣子可復用性才會高

🎇接下來會介紹基於對象來實現繼承的兩種方案,這兩種方案對於最後一種方案的出現有很大的幫助,請大家仔細看看理解


04-原型式繼承

這個方案是由道格拉斯·克羅克福德(Douglas Crockford)所提出來的,是對我們前端屆貢獻特別大的一個人物

這個方案是專門針對對象

我們先看下他當時是怎麼實現的

// o是對象
function createObject(o){
    function fn(){};
    //將對象賦值給fn的原型
    fn.prototype=o;
    //將實例化的fn賦值給新對象,這樣新對象的隱式原型就會指向我們傳遞進來的obj
    var newObj=new fn();
    return newObj;
}

大家可以瀏覽下我上面寫的程式碼,這是道格拉斯當時的想法

現在隨著js的逐漸變化,我們寫這種方案也變得比較簡單了,我們來看下面程式碼

function createObject(o){
    var newObj={};
    Obejct.setPrototypeof(newObj,o);
    return newObj;
}

Object.setPrototypeof()可以給對象設置一個新的原型,相比之前更加方便了

基於最新的ECMA標準的化,我們還可以這麼寫

var newObj=Object.create(o);

直接創建一個以o為原型的對象

🧨接下來是針對這一種方案做的一個完善版


05-寄生式繼承

var personObj = {
  running: function() {
    console.log("running")
  }
}
function createStudent(name) {
  var stu = Object.create(personObj)
  stu.name = name
  stu.studying = function() {
    console.log("studying~")
  }
  return stu
}
var stuObj = createStudent("why")
var stuObj1 = createStudent("kobe")
var stuObj2 = createStudent("james")

這種方案是基於第四種方案進行改編的

寄生式繼承的思路是結合原型類繼承和工廠模式的一種方式;

即創建一個封裝繼承過程的函數, 該函數在內部以某種方式來增強對象,最後再將這個對象返回;

✨好,現在結合上述五種方案,我們可以開始介紹最後一種方案啦,只有了解了上述五種方案後,再來看這種方案,會有一種恍然大悟的感覺🧑


06-寄生組合式繼承

這種方案就是JS社區這麼多年積累出來的最佳方案,我們來看下

//因為繼承是一個可能很常用的方法,所以我們把繼承的方法提煉出來了,作為一個工具函數
//這樣我們只需要傳遞父類和子類,函數就能自動幫助我們實現繼承
function inherit(faObj, chiObj) {
    //因為父類和子類的原型都是對象,所以可以用這個方法
    //創建一個以父類原型對象為原型的對象,並且將它賦值給子類原型
    //這幫助我們解決了第二種方案的父類執行了兩次的問題,不需要實例化父類對象
    chiObj.prototype = Object.create(faObj.prototype);
    //這是為了解決前面方案都沒有解決的constructor的方法
    Object.defineProperty(chiObj.prototype, "constructor", {
        value: chiObj,
        enumerable: false,
        writable: false,
        configurable: false,
    })
}

function Person(name, age, height) {
    this.name = name;
    this.age = age;
    this.height = height;
}
//下面是我們講的第二種方案 借用構造函數實現繼承
function Student(name, age, height, sno) {
    Person.call(this, name, age, height);
    this.sno = sno;
}
function Teacher(name, age, height, pro) {
    Person.call(this, name, age, height);
    this.pro = pro;
}

//在這裡使用我們自己定義的繼承函數
inherit(Person, Student);
inherit(Person, Teacher);
//注意:上下的順序不能錯,我在第二種方案的時候有講過
Person.prototype.playing = function () {
    console.log(this.name + " is playing");
}
Student.prototype.studying = function () {
    console.log(this.name + " is studying");
}
Teacher.prototype.teaching = function () {
    console.log(this.name + " is teaching");
}
var s1 = new Student("czx", 18, 1.88, 180047101);
var t1 = new Teacher("Mr.cao", 21, 1.88, "computer");

//可以列印出所有的屬性 並且列印的是對應的類,而不是父類了
console.log(s1);  // Student{}
console.log(t1); // Teacher{}
s1.studying();
t1.teaching();
s1.playing();
t1.playing();

我在程式碼中寫的很詳細了,大家一定要好好看看下,理解理解,如果覺得有啥疑惑,可以把之前的幾種方案連起來好好看看

如果還有什麼疑惑,歡迎大家在評論區留言😁😁😁

Tags: