JavaScript語言與傳統的面嚮對象語言(如Java)有點不同,js語言設計的簡單靈活,沒有class、namespace等相關概念,而是萬物皆對象。雖然js不是一個純正的面嚮對象語言,但依然能夠對js面向對象編程。java語言面向對象編程的基礎是類,而js語言面向對象編程的基礎是原型
。javascript
原型是學習js的基礎之一,由它衍生出許多像原型鏈、this指向、繼承等問題。因此深刻掌握js原型,才能對其衍生的問題有很好的理解。網上有不少文章解釋原型裏的等式關係,那樣有些晦澀難懂,這裏筆者從js設計歷史來逐步解釋js原型。html
在ES6前,js語法是沒有class
的。倒不是js語言做者Brendan Eich忘記引入class語法,而是由於當初設計js語言時,只想解決表單驗證等簡單問題(估計js做者沒想到後來js成爲最流行的語言之一),不必引入class這種重型武器,否則就跟Java這種基於class的面嚮對象語言同樣了。具體能夠看下阮一峯老師的Javascript繼承機制的設計思想。java
雖然設計js語言時,更多的考慮輕量級靈活,但依然要在語言層面考慮對象封裝以及多個對象之間複用的問題。先看下使用傳統方式進行封裝:git
function Person(name) {
return {
name: name,
sleep: function() {
console.log( 'go to sleep' )
}
}
}
var person1 = Person('tom')
var person2 = Person('lucy')
... personN
複製代碼
傳統方式有如下兩個弊端:github
既然無心引入class語法,同時須要知足對象的封裝以及複用問題,那就須要在js語言層面引入一種機制來處理class問題。編程
js做者使用了原型概念來解決class問題
。那什麼是原型?原型是如何在語法層實現的?會涉及到哪些概念?app
js原型概念通俗講有點像Java中的類概念,多個實例是基於共同的類型定義,就像tom、lucy這些真實的人(佔據空間)基於Person概念(不佔空間,只是定義)。java中類是基於class關鍵字的,但js中沒有class關鍵字,有的是function。而java類定義中都有個構造函數,實例化對象時會執行該構造函數,因此js做者簡化把構造函數constructor做爲原型(代替class)的定義
。同時規定構造函數須要知足如下條件:ide
// java定義類
class Person {
// java類中都有構造函數
constructor(name) {
this.name = name
}
public void sleep() {
....
}
}
複製代碼
// js使用構造函數代替類的做用
function Person(name) {
this.name = name
this.sleep = function() { ... }
}
複製代碼
以上經過構造函數定義了Person這個類。但如何區分一個function定義是構造函數仍是普通函數?難道是看定義的函數裏面是否有this來判斷?函數
固然不是,js做者引入new關鍵字來解決該問題
。由於構造函數只是定義原型(不佔據內存),最終仍是須要產生實例(佔據內存)來處理流程,因此使用new關鍵字來產生實例。同時規定new後面跟的是構造函數,而不是普通函數。這樣就區分出定義的function,是構造函數仍是普通函數。學習
// new 關鍵字後跟上構造函數,生成實例
// 語法層面上也和Java實例類一致
var tom = new Person('tom')
var lucy = new Person('lucy')
複製代碼
你確定注意到構造函數中this的疑問
,它究竟是在哪定義的?this又表明什麼?其實在執行new的過程當中,它會發生如下一些事:
// 模擬new的實現
function objectFactory() {
var obj = new Object(),
Constructor = [].shift.call(arguments); // 取出第一個參數,即構造函數
obj.__proto__ = Constructor.prototype;
var ret = Constructor.apply(obj, arguments);
return typeof ret === 'object' ? ret : obj;
};
var tom = objectFactory(Person, 'tom')
// 賦值的this === tom
console.log(tom.name) // tom
console.log(tom.sleep) // Function
複製代碼
new 和 constructor 解決了模擬class類的概念,使得產生的多個實例對象有共同的原型,同類型對象內在有了一些聯繫。看上去很完美,但還有個問題:每一個實例對象本質上仍是拷貝了構造函數對象裏的屬性和方法。tom和lucy實例的sleep方法依然建立了兩個內存空間進行存儲,而不是一個。這樣不只沒法數據共享,對內存的浪費也是極大的(想象下再生成10000個tom)。那js做者是如何解決這個問題的?
Brendan Eich爲構造函數設置一個prototype屬性來保存這些公用的方法或屬性
。prototype屬性是一個對象,你能夠擴展該對象,也能夠覆寫該對象。當你經過new constructor() 生成實例時,這些實例的公用方法(如:tom.sleep方法)並不會在內存中建立多份,而是經過指針都指向構造函數的prototype屬性(如:Person.prototype)。
注意:Person構造函數和Person.prototype都是對象,擁有諸多屬性。而且對象的屬性依然能夠是對象,萬物皆對象核心。
function Person(name) {
this.name = name
}
// 構造函數都有一個非空的prototype對象
// 能夠擴展該對象,也能夠覆寫該對象,如下在原型上擴展sleep方法
Person.prototype.sleep = function() { ... }
var tom = new Person('tom')
var lucy = new Person('lucy')
tom.sleep === lucy.sleep // true
複製代碼
因爲全部實例對象共享同一個prototype對象(構造函數的prototype屬性),那麼從外界看起來,prototype對象就好像是實例對象的原型,而實例對象則好像"繼承"了prototype對象同樣。這就是咱們通俗講的:js面向對象編程是基於原型
。
Javascript規定,每個構造函數都有一個prototype屬性,指向另外一個對象。這個對象的全部屬性和方法,都會被構造函數的實例繼承。
咱們再深刻思考下,js是如何把各個實例跟構造函數的prototype對象(如下稱原型對象)聯繫起來的?它們之間的通道是如何創建起來的?答案是使用new關鍵字。在上面咱們模擬new關鍵字流程中,有個步驟是: 添加__proto__屬性到tom實例上,而且把tom.__proto__指向Person.prototype。因此能夠獲得結論:實例與原型對象間關聯起來是經過__proto__屬性
。
__proto__屬性有什麼用?當訪問實例對象的屬性或方法時,若是沒有在實例上找到,js會順着__proto__去查找它的原型,若是找到就返回。因爲原型對象(如Person.prototype)也是一個對象,它也能夠有本身的原型對象(好比覆寫它),這樣層層上溯,就造成了一個相似鏈表的結構,這就是原型鏈(prototype chain)
。而經過覆寫子類原型對象,再根據js原型鏈機制,可讓子類擁有父類的內容,就像繼承同樣,因此原型鏈是js繼承的基礎
。
tom.__proto__ === lucy.__proto__ === Person.prototype // true
tom.sleep() // sleep方法是在原型鏈上找到的
複製代碼
注意new關鍵字以及原型鏈查找都是js語言內置的