上一篇咱們介紹了經過構造函數和原型能夠實現JavaScript中的「類」,因爲構造函數和函數的原型都是對象,因此JavaScript的「類」本質上也是對象。這一篇咱們將介紹JavaScript中的一個重要概念原型鏈,以及如何經原型鏈實現JavaScript中的繼承。javascript
首先,咱們簡單描述一下繼承的概念:當一個類和另外一個類構成"is a kind of"關係時,這兩個類就構成了繼承關係。繼承關係的雙方分別是子類和基類,子類能夠重用基類中的屬性和方法。java
C#能夠顯式地定義class,也可讓一個class直接繼承另一個class,下面這段代碼就是一個簡單的繼承。編程
public class Person { public string Name { get { return "keepfool"; } } public string SayHello() { return "Hello, I am " + this.Name; } } public class Employee : Person { public string Email { get; set; } }
因爲Employee類是繼承Person類的,因此Employee類的實例可以使用Person類的屬性和方法。函數
Employee emp = new Employee(); Console.WriteLine(emp.Name); Console.WriteLine(emp.SayHello()); Console.WriteLine("emp{0}是Person類的實例", emp is Person ? "" : "不");
emp是Employee類的一個實例,同時也是Person類的實例,它能夠訪問定義在Person類的Name屬性和SayHello()方法。post
這是C#的繼承語法,JavaScript則沒有提供這樣的語法,如今咱們來探討如何在JavaScript中實現繼承。性能
在JavaScript中定義兩個構造函數Person()和Employee(),爲了方便理解和講解,咱們能夠將它們理解爲Person類和Employee類。
如下內容提到的Person類、Employee類,和Person()構造函數、Employee()構造函數是一個意思。this
function Person() { this.name = 'keefool'; this.sayHello = function() { return 'Hello, I am ' + this.name; } } function Employee(email) { this.email = email; } var person = new Person(); var emp = new Employee('keepfool@xxx.com');
目前Person()和Employee()構造函數是兩個彼此獨立的存在,它們沒有任何關係。
因此由Employee()構造函數建立的實例emp,確定是訪問不到Person的name屬性和sayHello()方法的。spa
使用instanceof操做符一樣能夠肯定emp是Employee類的實例,而不是Person類的實例。prototype
實現繼承的目的是什麼?固然是讓子類可以使用基類的屬性和方法。
在這個示例中,咱們的目的是實現Employee繼承Person,而後讓Employee的實例可以訪問Person的name和sayHello()了。3d
JavaScript是如何實現繼承的呢?
這個答案有不少種,這裏我先只介紹比較常見的一種——經過原型實現繼承。
當咱們定義函數時,JavaScript會自動的爲函數分配一個prototype屬性。
Person()也是一個函數,那麼Person()函數也會有prototype屬性,即Person.prototype。
function Person() { this.name = 'keefool'; this.sayHello = function() { return 'Hello, I am ' + this.name; } } // 定義了函數後,JavaScript自動地爲Person()函數分配了一個prototype屬性 // Person.prototype = {};
咱們能夠在Person.prototype上定義一些屬性和方法,這些屬性和方法是能夠被Person的實例使用的。
function Person() { this.name = 'keefool'; this.sayHello = function() { return 'Hello, I am ' + this.name; } } Person.prototype.height = 176; var person = new Person(); // 訪問Person.prototype上定義的屬性 person.height; // 輸出176
同理在Employee.prototype上定義的屬性和方法,也能夠被Employee類的實例使用。
我們的目的是讓Employee的實例可以訪問name屬性和sayHello()方法,若是沒有Person()構造函數,我們是這麼作的:
function Employee(email) { this.email = email; } Employee.prototype = { name : 'keefool', sayHello = function() { return 'Hello, I am ' + this.name; } }
既然Person()構造函數已經定義了name和sayHello(),咱們就沒必要這麼作了。
怎麼作呢?讓Employee.prototype指向一個Person類的實例。
function Person() { this.name = 'keefool'; this.sayHello = function() { return 'Hello, I am ' + this.name; } } function Employee(email) { this.email = email; } var person = new Person(); Employee.prototype = person; var emp = new Employee('keepfool@xxx.com');
如今咱們就能夠訪問emp.name和emp.sayHello()方法了。
在Chrome控制檯,使用instanceof操做符,能夠看到emp對象如今已是Person類的實例了。
若是你對這段代碼仍是有所疑惑,你能夠這麼理解:
var person = new Person(); Employee.prototype.name = person.name; Employee.prototype.sayHello = person.sayHello;
因爲person對象在後面徹底沒有用到,以上這兩行代碼能夠合併爲一行。
function Person() { this.name = 'keefool'; this.sayHello = function() { return 'Hello, I am ' + this.name; } } function Employee(email) { this.email = email; } Employee.prototype = new Person(); var emp = new Employee('keepfool@xxx.com');
下面這幅圖歸納了實現Employee繼承Person的過程:
name和sayHello()不是Employee類的自有屬性和方法,它來源於Employee.prototype。
而Employee.prototype指向一個Person的實例,這個實例是可以訪問name和sayHello()的。
JavaScript的原型繼承的本質:將構造函數的原型對象指向由另一個構造函數建立的實例。
這行代碼Employee.prototype = new Person()
描述的就是這個意思。
如今咱們能夠說Employee()構造函數繼承了Person()構造函數。
用一句話歸納這個繼承實現的過程:
上一篇文章有提到過,每一個對象都有constructor屬性,constructor屬性應該指向對象的構造函。
例如:Person實例的constructor屬性是指向Person()構造函數的。
var person = new Person();
在未設置Employee.prototype時,emp對象的構造函數本來也是指向Employee()構造函數的。
當設置了Employee.prototype = new Person();
時,emp對象的構造函數卻指向了Person()構造函數。
無形之中,emp.constructor被改寫了。
emp對象看起來不像是Employee()構造函數建立的,而是Person()構造函數建立的。
這不是咱們指望的,咱們但願emp對象看起來也是由Employee()構造函數建立的,即emp.constructor應該是指向Employee()構造函數的。
要解決這個問題,咱們先弄清楚對象的constructor屬性是從哪兒來的,知道它是從哪兒來的就知道爲何emp.constructor被改寫了。
當咱們沒有改寫構造函數的原型對象時,constructor屬性是構造函數原型對象的自有屬性。
例如:Person()構造函數的原型沒有改寫,constructor是Person.prototype的自有屬性。
當咱們改寫了構造函數的原型對象後,constructor屬性就不是構造函數原型對象的自有屬性了。
例如:Employee()構造函數的原型被改寫後,constructor就不是Person.prototype的自有屬性了。
Employee.prototype的constructor屬性是指向Person()構造函數的。
這說明:當對象被建立時,對象自己沒有constructor屬性,而是來源於建立對象的構造函數的原型對象。
即當咱們訪問emp.constructor時,實際訪問的是Employee.prototype.constructor,Employee.prototype.constructor實際引用的是Person()構造函數,person.constructor引用是Person()構造函數,Person()構造函數其實是Person.prototype.constructor。
這個關係有點亂,咱們能夠用如下式子來表示這個關係:
emp.constructor = person.constructor = Employee.prototype.constructor = Person = Person.prototype.constructor
它們最終都指向Person.prototype.constructor!
弄清楚了對象的constructor屬性的來弄去脈,上述問題就好解決了。
解決辦法就是讓Employee.prototype.constructor指向Employee()構造函數。
var o = {}; function Person() { this.name = 'keefool'; this.sayHello = function() { return 'Hello, I am ' + this.name; } } function Employee(email) { this.email = email; } Employee.prototype = new Person(); Employee.prototype.constructor = Employee; var emp = new Employee('keepfool@xxx.com');
若是你仍是不能理解關鍵的這行代碼:
Employee.prototype.constructor = Employee;
你能夠嘗試從C#的角度去理解,在C#中Employee類的實例確定是由Employee類的構造函數建立出來的。
原型鏈是JavaScript中很是重要的概念,理解它有助於理解JavaScript面向對象編程的本質。
定義函數時,函數就有了prototype屬性,該屬性指向一個對象。
prototype屬性指向的對象是共享的,這有點像C#中的靜態屬性。
站在C#的角度講,由new建立的對象是不能直接訪問類的靜態屬性的。
那麼在JavaScript中,爲何對象可以訪問到prototype中的屬性和方法的呢?
例如:當emp對象被建立時,JavaScript自動地爲emp對象分配了一個__proto__屬性,這個屬性是指向Employee.prototype的。
在Chrome的控制檯查看emp.__proto__
的內容
首先,▼Person {name: "keepfool"}
表示emp.__proto__是一個Person對象,由於Employee.prototype確實指向一個Person對象。
其次,咱們把emp.__proto__的屬性分爲3個部分來看。
Employee.prototype.constructor = Employee
,因此constructor是指向Employee()構造函數的。對象的__proto__屬性就像一個祕密連接,它指向了建立該對象的構造函數的原型對象。
咱們注意到第3部分的內容仍然是一個__proto__屬性,咱們展開它看個究竟吧。
再往下看,還有兩層__proto__。
emp.__proto__.__proto__:從▶constructor:function Person()
能夠看出它是Person()構造函數的原型。
Person.prototype包含兩部份內容:
咱們將這一系列的__proto__
稱之爲原型鏈。
下面兩幅圖展現了本文示例的原型鏈,這兩幅圖表示的同一個意思。原型鏈的最頂端是null,由於Object.prototype是沒有__proto__屬性。
下表清晰地描述了每一層__proto__表示的內容:
編號 | 原型鏈 | 原型鏈指向的對象 | 描述 |
---|---|---|---|
1 | emp.__proto__ | Employee.prototype | Employee()構造函數的原型對象 |
2 | emp.__proto__.__proto__ | Person.prototype | Person()構造函數的原型對象 |
3 | emp.__proto__.__proto__.__proto__ | Object.prototype | Object()構造函數的原型對象 |
4 | emp.__proto__.__proto__.__proto__.__proto__ | null | 原型鏈的頂端 |
如今能夠解釋emp對象可以訪問到name屬性和sayHello()方法了。
以訪問emp.sayHello()爲例,咱們用幾個慢鏡頭來闡述:
另外,在實現Employee()繼承Person(),以及emp對象訪問name和sayHello()時,JavaScript是幫咱們作了一些事情的,見下圖:
上一篇有提到過,Person類的sayHello()方法放到它的原型對象中更合適,這樣全部的Person實例共享一個sayHelo()方法副本,若是咱們把這個方法提到原型對象會發生什麼?
var o = {}; function Person() { this.name = 'keefool'; } Person.prototype.sayHello = function(){ return 'Hello, I am ' + this.name; } function Employee(email) { this.email = email; } Employee.prototype = new Person(); Employee.prototype.constructor = Employee; var emp = new Employee('keepfool@xxx.com');
能夠看到sayHello()方法的路徑是:emp.__proto__.__proto__.sayHello()
,比直接定義在Person()構造函數中多了一層。
這樣看來將方法定義在原型對象中並非絕對的好,會使得JavaScript遍歷較多層數的原型鏈,這也會有一些性能上的損失。
爲了增強對原型鏈的理解,咱們來作個簡單的示例吧。
上圖已經說明了toString()方法是屬於內置的Object對象的,咱們以toString()方法來說解這個示例。
在Chrome控制檯輸入emp.toString(),咱們獲得的結果是"[object Object]"
。
toString()方法是在emp的第3層原型鏈找到的,即emp.__proto__.__proto__.__proto__
,它就是Object對象。
emp.toString()輸出"[object Object]"
沒有什麼意義,如今咱們在Person.prototype上定義一個toString()方法。
var o = {}; function Person() { this.name = 'keefool'; } Person.prototype.sayHello = function(){ return 'Hello, I am ' + this.name; } Person.prototype.toString = function() { return '[' + this.name + ']'; } function Employee(email) { this.email = email; } Employee.prototype = new Person(); Employee.prototype.constructor = Employee; var emp = new Employee('keepfool@xxx.com');
這時toString()方法是在emp對象的第2層原型鏈找到的,即emp.__proto__.__proto__
。emp.__proto__.__proto__
是Person()構造函數的原型對象,即Person.prototype。
這個也是一個簡單的重寫示例,Person.protoype重寫了toString()方法,emp最終調用的是Person.prototype.toString()方法。