JS進階(2):人人都能懂的原型物件

零和么發表於2018-03-18

封面.jpeg

凡是搞前端開發的或者玩 JavaScript 的同學都知道,原型物件和原型鏈是 JavaScript 中最為重要的知識點之一,也是前端面試必問的題目,所以,掌握好原型和原型鏈勢在必行。因此,我會用兩篇文章(甚至更多)來分別講解原型物件以及原型鏈。

在上一篇文章中,我們詳細介紹了建構函式的執行過程以及返回值,如果沒有看的同學,請點選連結 JS進階(1): 人人都能懂的建構函式 閱讀,因為這是本篇文章的基礎知識。

廢話不多說,進入正題。

一、為什麼要使用原型物件

通過上一篇文章的介紹,我們知道:

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

var p1 = new Person('Tom', 18);
var p2 = new Person('Jack', 34);
console.log(p1.name, p1.age);   // 'Tom', 18
console.log(p2.name, p2.age);   // 'Jack', 34
複製程式碼

但是,在一個物件中可能不僅僅存在屬性,還存在方法:

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.say = function() {
        console.log('Hello');
    };
}

var p1 = new Person('Tom', 18);
p1.say();  // 'Hello'
var p2 = new Person('Jack', 34);
p2.say();  // 'Hello'
複製程式碼

我們發現,例項 p1 和 例項 p2 呼叫了相同的方法,都列印出 Hello 的結果。但是,它們的記憶體地址是一樣的麼?我們列印看看:

console.log(p1.say == p2.say); // false
複製程式碼

結果當然為 false 。因為我們在上一篇文章中就說過,每一次通過建構函式的形式來呼叫時,都會開闢一塊新的記憶體空間,所以例項 p1p2 所指向的記憶體地址是不同的。但此時又會有一個尷尬的問題,p1p2 呼叫的say 方法,功能卻是相同的,如果班裡有 60 個學生,我們需要呼叫 60 次相同方法,但卻要開闢 60 塊不同的記憶體空間,這就會造成不必要的浪費。此時,原型物件就可以幫助我們解決這個問題。

二、如何使用原型物件

當一個函式 (注意:不僅僅只有建構函式) 建立好之後,都會有一個 prototype 屬性,這個屬性的值是一個物件,我們把這個物件,稱為原型物件。同時,只要在這個原型物件上新增屬性和方法,這些屬性和方法都可以被該函式的例項所訪問。

原型物件1.png

既然,函式的例項可以訪問到原型物件上的屬性和方法,那我們不妨把上面的程式碼改造一下。

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

Person.prototype.say = function() {
    console.log('Hello');
};

var p1 = new Person('Tom', 18);
var p2 = new Person('Jack', 34);

console.log(p1.say === p2.say); // true
複製程式碼

此時,我們看到例項 p1 和 例項 p2say 指向同一塊記憶體空間。這是什麼原因呢?我們通過控制檯的列印結果來看看。

原型物件2.png

通過上面的截圖我們可以看到,Person.prototypep1.__proto__p2.__proto__ 似乎是一樣的。為了驗證我們的猜想,我們試著在列印:

Person.prototype === p1.__proto__;   // true
Person.prototype === p2.__proto__;   // true
p1.__proto__ === p2.__proto___;      // true
複製程式碼

我們發現,所有的結果都為 true 。 而這正好解釋了為什麼 p1.say === p2.say 為 true 。

三、繪製 建構函式——原型物件——例項 關係圖

現在你大概理解了原型物件,也知道了使用原型物件有什麼好處。下面我們通過繪製圖形的方式再來深刻地理解一下上面的過程。

我們就以下面的程式碼為例:

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

Person.prototype.say = function() {
    console.log('I am saying');
}

var p1 = new Person('Tom');
複製程式碼

1. Person 函式建立之後,會產生一塊記憶體空間,並且有一個 prototype 屬性

原型物件3.png

2. prototype 屬性的值是一個物件,我們稱之為原型物件

原型物件4.png

3. 原型物件中的屬性和方法

參照上面控制檯的截圖,我們可以知道:

(1)原型物件上,有一個 constructor 屬性指向 Person; (2)原型物件上,有一個 say 方法,會開闢一塊新的記憶體空間; (3)原型物件上,有一個 __proto__ 屬性,這個我們下篇文章再來解釋。

根據上面我們的分析,繼續繪製:

原型物件5.png

4. 例項中的屬性和方法

p1 這個例項建立好之後,又會開闢一塊新的記憶體空間。此時,依舊參照上面控制檯的截圖,我們可以知道:

(1)p1 例項中有一個 name 屬性; (2)p1 例項中有一個 __proto__ 屬性,指向建構函式 Person 的原型物件。

根據上面的分析,我們繼續繪製:

原型物件6.png

四、總結

通過上面的解釋,大家應該可以理解原型物件是什麼以及為什麼要使用原型物件了。最後,我們來總結一下本文的核心知識點。

  1. 一個函式建立好之後,就會有一個 prototype 屬性,這個屬性的值是一個物件,我們把這個 prototype 屬性所指向的記憶體空間稱為這個函式的原型物件。

  2. 某個函式的原型物件會有一個 constructor 屬性,這個屬性指向該函式本身。

function Person() {
    // ...
}
console.log(Person.prototype.constructor === Person); // true
複製程式碼
  1. 當某個函式當成建構函式來呼叫時,就會產生一個建構函式的例項。這個例項上會擁有一個 __proto__ 屬性,這個屬性指向該例項的建構函式的原型物件(也可以稱為該例項的原型物件)。
function Person() {
    // ...
}
var p1 = new Person();
console.log(p1.__proto__ === Person.prototype); // true
複製程式碼

最後,本文描述的僅僅是一個建構函式——原型物件——例項的關係圖,並不是完整的原型鏈。大家可以先理解這一部分,等到講解原型鏈的時候,我會繪製一張完整的原型鏈圖供大家理解。童鞋們可以先試著理解今天的文章,並且自己繪製一下建構函式——原型物件——例項的關係圖,相信你的收穫將會更大。

最後的最後,我所說的不一定都對,你一定要自己試試!

(本文完)

相關文章