1. 程式人生 > >JavaScript原型,原型鏈,繼承

JavaScript原型,原型鏈,繼承

原型

原型的作用:
1、通過原型來新增方法,解決資料共享,節省記憶體空間
2、作用於繼承,模擬面向物件,來做到繼承,多型

建構函式

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

通過原型新增方法:

Person.prototype.eat=function () {
        console.log("吃飯");
    };

例項化物件:

var per1=new Person("小明","18");
var per2=new Person("小紅"
,"20");

接下來就是我們所要探討的了,per1和per2的eat的這個方法是不是同一個方法呢?
我們來驗證一下:

console.log(per1.eat==per2.eat);//true

結果是true


我們來檢視一下他們的結構
console.dir(per1);
console.dir(per2);

這裡寫圖片描述

結論:我們可以看到在Person結構目錄下他們都有age和name屬性,同時他們也都有 _ proto _ 這個屬性,_ proto _
下面我們看到了又eat這個方法了,這樣做到了簡單的資料共享了,至於後面的constructor和_ proto _: Object是有關於
原型鏈和繼承;請看下文,我們繼續深入

我們雖然做到了同一個建構函式內共享方法且節省了記憶體空間,但是這並不是我們的真正目的

下面我們再來一個建構函式

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

為原型新增方法

Student.prototype.study=function () {
      console.log("學習");
    };

例項化Student物件

var stu=new Student("小紅",17,"女");

那麼問題來了如何讓stu可以訪問per的方法呢?答案是可以的我們可以通過改變Student的原型指向來做到

Student.prototype=new Person("小明","18");

輸出一下,用stu去訪問Person的方法
stu.eat;//吃飯

通過改變原型的指向我們可以去訪問別的建構函式的方法但是這樣做湧現出來一個問題,那就是通過改變原型指向後,
Student沒辦法訪問自己的原本的方法study了

console.log(stu.study);//undefined
stu.study();// 報錯

為什麼會出現這個問題呢?讓我們想一下,我們是先給Student新增的原型指向然後才改變的原型指向,Student的原型指向本來是study,但是之後又被指向了new Person,所以自然Student的原型方法study被架空了,如果我們換個方式來寫,
先給Student改變原型指向,再新增方法這樣的話study這個方法就不會被架空了

我把程式碼重新集中一下(方便閱讀)

 //人的建構函式
    function Person(age) {
      this.age=10;
    }
    //人的原型物件方法
    Person.prototype.eat=function () {
      console.log("人");
    };
    //學生的建構函式
    function Student() {

    }
    Student.prototype=new Person(10);
    Student.prototype.sayHi=function () {
      console.log("學生");
    };

輸出:

stu.eat(); //人
stu.sayHi(); //學生

檢視結構:console.dir(stu);
這裡寫圖片描述



於是推出以下結論:

當呼叫某種方法或查詢某種屬性時,首先會在自身呼叫和查詢,如果自身並沒有該屬性或方法,則會去它的_ proto _屬性中呼叫查詢,也就是它建構函式的prototype中呼叫查詢


附一張原型截個圖(圖片來自於網路)
這裡寫圖片描述

繼承

其實上面那種方式就繼承中的其中一種——原型繼承了;為什麼要到現在才說呢,就是為了結合繼承對原型的理解更加深入更加易於理解,下面我將繼續闡述幾種繼承並指出他們的優缺點和相應的案例

構造繼承
核心:使用父類的建構函式來增強子類例項,等於是複製父類的例項屬性給子類(沒用到原型)

function Cat(name){
  Animal.call(this);
  this.name = name || 'Tom';
}

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true

特點:

  1. 解決了1中,子類例項共享父類引用屬性的問題
  2. 建立子類例項時,可以向父類傳遞引數
  3. 可以實現多繼承(call多個父類物件)

缺點:

  1. 例項並不是父類的例項,只是子類的例項
  2. 只能繼承父類的例項屬性和方法,不能繼承原型屬性/方法
  3. 無法實現函式複用,每個子類都有父類例項函式的副本,影響效能



例項繼承
核心:為父類例項新增新特性,作為子類例項返回

function Cat(name){
  var instance = new Animal();
  instance.name = name || 'Tom';
  return instance;
}

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // false

特點:
1. 不限制呼叫方式,不管是new 子類()還是子類(),返回的物件具有相同的效果

缺點:

  1. 例項是父類的例項,不是子類的例項
  2. 不支援多繼承



    拷貝繼承

    var obj1={
        name:10,
        age:"女",
        sleep:function () {
            console.log("睡覺")
        }
    };
    //改變了地址指向
    var obj2=obj1;
    console.log(obj2.name,obj2.age,);
    obj2.sleep();
//這個拷貝只是改變了棧的指向
//而下面方法,是把堆裡面的屬性和方法複製一份重新開了一個空間


function Cat(name){
  var animal = new Animal();
  for(var p in animal){
    Cat.prototype[p] = animal[p];
  }
  Cat.prototype.name = name || 'Tom';
}

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // tru

特點:

  1. 支援多繼承

缺點:

  1. 效率較低,記憶體佔用高(因為要拷貝父類的屬性)
  2. 無法獲取父類不可列舉的方法(不可列舉方法,不能使用for in 訪問到)


    組合繼承(常用的繼承方式)
    核心:通過呼叫父類構造,繼承父類的屬性並保留傳參的優點,然後通過將父類例項作為子類原型,實現函式複用

//原型實現繼承
    //借用建構函式實現繼承
    //組合繼承:原型繼承+借用建構函式繼承
    function Person(name,age,sex) {
        this.name=name;
        this.age=age;
        this.sex=sex;
    };
    Person.prototype.sayHi=function () {
      console.log("人的打招呼");
    };
    function Student(name,age,sex,score) {
        //借用建構函式:屬性值重複的問題
        Person.call(this,name,age,sex)
        this.score=score;
    };
    //改變原型指向----繼承
    Student.prototype=new Person();//不傳值
    Student.prototype.eat=function () {
        console.log("學生的吃原型");
    };
    var stu1=new Student("小明","18","男","100");
    console.log(stu1.name,stu1.age,stu1.sex,stu1.score);//小明 18 男 100
    stu1.sayHi();//人的打招呼
    stu1.eat();//學生的吃原型

特點:

  1. 彌補了方式2的缺陷,可以繼承例項屬性/方法,也可以繼承原型屬性/方法
  2. 既是子類的例項,也是父類的例項
  3. 不存在引用屬性共享問題
  4. 可傳參
  5. 函式可複用

缺點:

  1. 呼叫了兩次父類建構函式,生成了兩份例項(子類例項將子類原型上的那份遮蔽了)




寄生組合繼承
核心:通過寄生方式,砍掉父類的例項屬性,這樣,在呼叫兩次父類的構造的時候,就不會初始化兩次例項方法/屬性,避免的組合繼承的缺點

function Cat(name){
  Animal.call(this);
  this.name = name || 'Tom';
}
(function(){
  // 建立一個沒有例項方法的類
  var Super = function(){};
  Super.prototype = Animal.prototype;
  //將例項作為子類的原型
  Cat.prototype = new Super();
})();

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); //true

特點:

  1. 堪稱完美

缺點:

  1. 實現較為複雜
    ———————————————————————分割線———————————————————————————



    附一點題外話:

    • 面向物件程式設計思想:根據需求,分析物件,找到物件有什麼特徵和行為,通過程式碼的方式來實現需求,要想實現這個需求,就要建立物件,要想建立物件,就應該顯示有建構函式,然後通過建構函式來建立物件.,通過物件呼叫屬性和方法來實現相應的功能及需求,即可

    • 首先JS不是一門面向物件的語言,JS是一門基於物件的語言,那麼為什麼學習js還要學習面向物件,因為面向物件的思想適合於人的想法,程式設計起來會更加的方便,及後期的維護….

    • 面向物件的程式語言中有類(class)的概念(也是一種特殊的資料型別),但是JS不是面向物件的語言,所以,JS中沒有類(class),但是JS可以模擬面向物件的思想程式設計,JS中會通過建構函式來模擬類的概念(class)

    • 所有引用型別(函式,陣列,物件)都擁有proto屬性(隱式原型)

    • 所有函式擁有prototype屬性(顯式原型)(僅限函式)

    • 原型物件:擁有prototype屬性的物件,在定義函式時就被建立


這裡寫圖片描述