凡是搞前端開發的或者玩 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
。因為我們在上一篇文章中就說過,每一次通過建構函式的形式來呼叫時,都會開闢一塊新的記憶體空間,所以例項 p1
和 p2
所指向的記憶體地址是不同的。但此時又會有一個尷尬的問題,p1
和 p2
呼叫的say
方法,功能卻是相同的,如果班裡有 60 個學生,我們需要呼叫 60 次相同方法,但卻要開闢 60 塊不同的記憶體空間,這就會造成不必要的浪費。此時,原型物件就可以幫助我們解決這個問題。
二、如何使用原型物件
當一個函式 (注意:不僅僅只有建構函式) 建立好之後,都會有一個 prototype
屬性,這個屬性的值是一個物件,我們把這個物件,稱為原型物件。同時,只要在這個原型物件上新增屬性和方法,這些屬性和方法都可以被該函式的例項所訪問。
既然,函式的例項可以訪問到原型物件上的屬性和方法,那我們不妨把上面的程式碼改造一下。
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
和 例項 p2
的 say
指向同一塊記憶體空間。這是什麼原因呢?我們通過控制檯的列印結果來看看。
通過上面的截圖我們可以看到,Person.prototype
與 p1.__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
屬性
2. prototype
屬性的值是一個物件,我們稱之為原型物件
3. 原型物件中的屬性和方法
參照上面控制檯的截圖,我們可以知道:
(1)原型物件上,有一個
constructor
屬性指向 Person; (2)原型物件上,有一個say
方法,會開闢一塊新的記憶體空間; (3)原型物件上,有一個__proto__
屬性,這個我們下篇文章再來解釋。
根據上面我們的分析,繼續繪製:
4. 例項中的屬性和方法
當 p1
這個例項建立好之後,又會開闢一塊新的記憶體空間。此時,依舊參照上面控制檯的截圖,我們可以知道:
(1)
p1
例項中有一個name
屬性; (2)p1
例項中有一個__proto__
屬性,指向建構函式Person
的原型物件。
根據上面的分析,我們繼續繪製:
四、總結
通過上面的解釋,大家應該可以理解原型物件是什麼以及為什麼要使用原型物件了。最後,我們來總結一下本文的核心知識點。
-
一個函式建立好之後,就會有一個
prototype
屬性,這個屬性的值是一個物件,我們把這個prototype
屬性所指向的記憶體空間稱為這個函式的原型物件。 -
某個函式的原型物件會有一個
constructor
屬性,這個屬性指向該函式本身。
function Person() {
// ...
}
console.log(Person.prototype.constructor === Person); // true
複製程式碼
- 當某個函式當成建構函式來呼叫時,就會產生一個建構函式的例項。這個例項上會擁有一個
__proto__
屬性,這個屬性指向該例項的建構函式的原型物件(也可以稱為該例項的原型物件)。
function Person() {
// ...
}
var p1 = new Person();
console.log(p1.__proto__ === Person.prototype); // true
複製程式碼
最後,本文描述的僅僅是一個建構函式——原型物件——例項的關係圖,並不是完整的原型鏈。大家可以先理解這一部分,等到講解原型鏈的時候,我會繪製一張完整的原型鏈圖供大家理解。童鞋們可以先試著理解今天的文章,並且自己繪製一下建構函式——原型物件——例項的關係圖,相信你的收穫將會更大。
最後的最後,我所說的不一定都對,你一定要自己試試!
(本文完)