一文搞懂JavaScript原型鏈(看完絕對懂)

學習目標

  • 原型
  • 原型鏈
  • 原型指向改變後是如何添加方法和屬性
  • 原型指向改變後的原型鏈
  • 實例對象的屬性和原型對象的屬性重名
  • 經過原型繼承
  • 組合繼承
  • 拷貝繼承

一,原型

問題: 請看如下代碼,若是咱們要建立100個對象,應該怎麼建立?前端

function Person(name, sex) {
    this.name = name;
    this.sex = sex;
    this.drink () {
        console.log('我想喝手磨咖啡!!')
    }
}
for (let i = 0; i < 100; i++) {
    var per = new Person('蘇大強', '男');
    per.drink();
}
複製代碼

從上面的代碼能夠看出,若是咱們要建立100個Person對象,這樣要開一百個內存空間,每次都要調用drink()函數,因爲drink()函數都是同樣的,每一個內存空間裏都有它太過於浪費空間,那咱們怎樣才能避免這種狀況,減小內存呢?咱們接下來引入原型prototype程序員

function Person(name, sex) {
    this.name = name;
    this.sex = sex;
}
//爲原型添加方法
Person.prototype.drink = function () {
        console.log('我想喝手磨咖啡!!')
    }
    //實例化對象
let per = new Person('蘇大強', '男');
per.drink();
複製代碼

咱們運用了原型prototype,能夠共享數據,減小內存空間。瀏覽器

二,原型鏈

咱們既然清楚了原型,那咱們再來看看原型鏈。首先咱們打印一下構造函數Person和實例對象per。bash

console.dir(Person);//構造函數
console.dir(per);//實例對象
複製代碼

從圖上看,構造函數中的prototype中的屬行和實例對象per中的__proto__中的屬性如出一轍,那咱們想一想它們相等嗎?咱們能夠驗證一下。

console.log(per.__proto__ === Person.prototype);
複製代碼

由此咱們能夠判斷出,構造函數Person中的prototype原型和實例對象per中的__proto__原型指向是相同的,咱們通常是先有構造函數再有實例對象,實例對象由構造函數建立,因此說實例對象中的__proto__原型指向的是構造函數中的原型prototype

實例對象中__proto__是原型,瀏覽器使用的。構造函數中的prototype是原型,程序員使用的函數

那接下來咱們看一幅圖來看看原型鏈究竟是什麼?學習

咱們來分析分析整張圖

  • 首先,構造函數中的prototype屬性指向本身的原型對象
  • 而後,原型對象中的構造器指向的是,原型對象所在的構造函數
  • 再而後,實例對象中的__proto__指向的是,它所在構造函數中prototype屬性所指向的原型對象

因此從上圖咱們能夠獲得如下幾點:ui

  • 實例對象的原型指向了構造函數中prototype屬性所指向的原型對象,因此實例對象和原型對象之間有關係,它和構造函數是一個間接的關係。
  • 咱們從代碼中也能夠得出,實例對象能夠直接訪問原型對象中的屬性或方法。
  • 實例對象和原型對象之間有關係,它們的關係是經過原型__proto__來鏈接的。

最終咱們能夠得出,原型鏈:它是一種關係,實例對象和原型對象之間的關係,關係是經過原型__proto__來聯繫的this

三,原型指向改變後是如何添加方法和屬性

原型改變添加方法也無非就是兩種:1.在原型改變前添加加方法。2.在原型改變之後添加方法。spa

首先,咱們來看第一種:prototype

function Person(name, sex) {
    this.name = name;
    this.sex = sex;
}
Person.prototype.drink = function () {
        console.log('我想喝水!!')
    }
function Student(name, sex) {
    this.name = name;
    this.sex = sex;
}
Student.prototype.eat = function () {
    console.log('我想吃東西!!')
}

//改變原型指向
Student.prototype = new Person('人', '男');
let stu = new Student('學生', '女');
stu.drink();
stu.eat();
複製代碼

咱們來運行如下:

咱們能夠看到圖中的信息,stu.eat()不是一個函數,剛纔咱們明明將eat()添加到了Student的原型上,怎麼如今報錯了? 緣由是:因爲Student的原型指向改變了,它指向了new Person('人', '男'),而且Person的原型上並無eat(),因此報錯,那麼第一種狀況在原型改變以前添加是錯誤的!

咱們再來看第二種狀況:在原型改變以後添加方法。

function Person(name, sex) {
    this.name = name;
    this.sex = sex;
}
Person.prototype.drink = function () {
        console.log('我想喝水!!')
    }
function Student(name, sex) {
    this.name = name;
    this.sex = sex;
}

//改變原型指向
Student.prototype = new Person('人', '男');
//爲原型添加方法
Student.prototype.eat = function () {
    console.log('我想吃東西!!')
}
let stu = new Student('學生', '女');
stu.drink();
stu.eat();
複製代碼

咱們來運行如下:

能夠看出來,兩個方法都被成功的調運了,因此說:若是原型指向改變了,那麼就應該在原型改變指向以後添加原型方法。

四,原型指向改變後的原型鏈

那麼,當原型指向改變以後,原型鏈會發生怎樣的改變呢? 那咱們來們分析如下:

  • 原型指向改變以前
  • 原型指向改變以後

咱們先來分析原型指向改變以前:

//人的構造函數
function Person(name) {
    this.name = name;
}
//爲原型添加方法
Person.prototype.drink = function () {
        console.log('我想喝水!!')
    }
//學生的構造函數
function Student(name) {
    this.name = name;
}
//爲原型添加方法
Student.prototype.eat = function () {
    console.log('我想吃東西!!')
}
//實例對象
let per = new Person('老師');
let stu = new Student('學生');

console.dir(Person);//構造函數
console.dir(per);//實例對象
console.dir(Student);//構造函數
console.dir(stu);//實例對象
複製代碼

咱們運行如下這段代碼:

請看每一個prototype和__proto__,咱們能夠獲得它們的原型鏈圖:

由此圖,咱們能夠看出,原型沒有改變以前,實例對象的__proto__都指向本身構造函數中prototype屬性所指向的原型對象。

咱們再來看看原型指向改變以後:

//人的構造函數
function Person(name) {
    this.name = name;
}
//爲原型添加方法
Person.prototype.drink = function () {
        console.log('我想喝水!!')
    }
//學生的構造函數
function Student(name) {
    this.name = name;
}
//爲原型添加方法
Student.prototype.eat = function () {
    console.log('我想吃東西!!')
}
//改變學生的原型指向
Student.prototype = new Person('老師');
//實例對象
let stu = new Student('學生');

console.dir(Person);//構造函數
console.dir(new Person('老師'))//實例對象
console.dir(Student.prototype)//Student的原型對象
console.dir(Student);//構造函數
console.dir(stu);//實例對象
複製代碼

咱們來看看運行結果:

咱們來分析分析:

咱們代碼和圖結合來看,當Student.prototype = new Person('老師');以後,①學生構造函數的prototype屬性會斷開指向原型對象,②原型對象中的構造器也會斷開指向構造函數,③實例對象的__proto__會斷開指向原型對象 這裏的序號沒有任何意義,至關於起的名字!!! 尚未完,咱們再來看圖:

當原型指向改變以後,學生的構造函數中的prototype屬性指向了new Person('老師');,隨後,學生的實例化對象中的__proto__屬性指向了學生構造函數中prototype屬性所指向的new Person('老師');

原型鏈改變完畢!

五,實例對象的屬性和原型對象的屬性重名

當實例對象中的屬性和原型對象中的屬性重名時應該先訪問那個? 咱們來看一看代碼:

//人的構造函數
function Person(age, sex) {
      this.age = age;
      this.sex = sex;
    }
//爲原型添加屬性
Person.prototype.sex = "女";
//實例化對象
var per = new Person(10,"男");

console.log(per.sex);
複製代碼

看圖:

從代碼的運行結果來看,當實例對象中的屬性和原型中的屬性重名時,它會先訪問實例對象中的屬性。

若是在實例對象中找不到呢?咱們來看代碼:

function Person(age) {
      this.age = age;
    }
//爲原型添加屬性
Person.prototype.sex = "女";
//實例化對象
var per = new Person(10);

console.log(per.sex);
複製代碼

咱們來看運行結果:

從這段代碼,咱們能夠看出,當實例對象中訪問不到屬性時,它會向上查找原型對象上的屬性

六,經過原型繼承

//js中經過原型來實現繼承
    
    //人的構造函數
    function Person(name, age, sex) {
      this.name = name;
      this.sex = sex;
      this.age = age;
    }
    //爲原型添加方法
    Person.prototype.eat = function () {
      console.log("人吃東西");
    };
    Person.prototype.sleep = function () {
      console.log("人在睡覺");
    };
    Person.prototype.play = function () {
      console.log("生活就是編代碼!");
    };

    
    //學生的構造函數
    function Student(score) {
      this.score = score;
    }
    //改變學生的原型的指向便可==========>學生和人已經發生關係
    Student.prototype = new Person("小明", 10, "男");
    //爲原型添加方法
    Student.prototype.study = function () {
      console.log("學習很累很累的哦.");
    };


    var stu = new Student(100);
    console.log(stu.name);
    console.log(stu.age);
    console.log(stu.sex);
    stu.eat();
    stu.play();
    stu.sleep();
    console.log("下面的是學生對象中本身有的");
    console.log(stu.score);
    stu.study();
複製代碼

看運行結果:

原型繼承的精髓就是:使用原型,改變原型的指向進行繼承。

七,組合繼承

//組合繼承:原型繼承+借用構造函數繼承
    
    //人的構造函數
    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 stu = new Student("金仔", 20, "男", "100分");
    console.log(stu.name, stu.age, stu.sex, stu.score);
    stu.sayHi();
    stu.eat();
    
    var stu2=new Student("含仔", 20, "女", "100分");
    console.log(stu2.name, stu2.age, stu2.sex, stu2.score);
    stu2.sayHi();
    stu2.eat();
複製代碼

看運行結果:

組合繼承的精髓:首先在Student構造函數中使用call()函數將屬性傳入Person構造函數中,並改變this,而後改變Student的原型指向

八,拷貝繼承

function Person() {};
    Person.prototype.age = 10;
    Person.prototype.sex = "男";
    Person.prototype.height = 100;
    Person.prototype.play = function () {
      console.log("玩耍!");
    };
    var Student = {};
    //Person的構造中有原型prototype,prototype就是一個對象,那麼裏面,age,sex,height,play都是該對象中的屬性或者方法

    for (let key in Person.prototype) {
      Student[key] = Person.prototype[key];
    }
    console.dir(Student);
    Student.play();
複製代碼

請看運行結果:

至此,本片文章的所有內容完畢!本人是一個前端新人,本片文章若哪裏有不正確的地方,請各位前端大佬不吝斧正!我們攜手共同進步!再次感謝!

另外,本人將要參加實習找工做,如果那位大佬以爲小學弟能夠的話,請給小學弟一個機會。郵箱:1441398660@qq.com

相關文章
相關標籤/搜索