JavaScript原型鏈及繼承

在JavaScript中,全部的東西都是對象,可是JavaScript中的面向對象並非面向類,而是面向原型的,這是與C++、Java等面嚮對象語言的區別,比較容易混淆,所以把我本身學習的過程記錄下來。編程

首先說,原型鏈有什麼用?在Java中,繼承都是基於類的,在JavaScript中繼承都是基於原型鏈的。也就是說在JavaScript中,原型鏈是實現繼承的基礎,想要掌握好JavaScript中的面向對象編程,必須對原型鏈有必定的瞭解。瀏覽器

要理解原型鏈,必須先了解兩個對象,一個是 prototype ,另外一個是 __proto__ 。當前只須要記住名字,下面會仔細說明。函數

首先是 prototype : prototype ,或者叫原型對象,是函數特有的一個屬性,其類型是 Object ,所以也經常被稱做函數的原型對象。雖然每一個函數都擁有本身的原型對象,但只有用做構造函數時,這個屬性纔會發揮做用,關於構造函數的知識這裏不說。原型對象其實很簡單,他就是一個普通的 Object ,當其做爲構造函數時默認有一個 constructor 。學習

1 // 舉個簡單的例子
2 function fn() { }
3 
4 console.log(fn.prototype);  // {}

咱們能夠給原型對象添加一些方法或屬性,就能夠被其子類繼承:this

 1 // 後面會講怎麼繼承
 2 function fn() { }
 3 
 4 // 添加一個方法
 5 fn.prototype.sayHello = function() {
 6     alert('hello');
 7 };
 8 
 9 // 添加一個屬性
10 fn.prototype.name = 'my_fn';

 上面就完成了對原型對象的介紹,接下來是 __proto__ ,這是一個全部對象都擁有的屬性。其實 __proto__ 與原型對象密不可分,由於一個對象的 __proto__ 就是指向其構造函數的原型對象。須要注意的是 __proto__ 並非JavaScript的規範,只是大多數瀏覽器都實現了,從ECMAScript 6開始,應該用Object.getPrototypeOf()和Object.setPrototypeOf()來訪問這個屬性。看一個例子:spa

1 function fn() { }
2 
3 var f = new fn();
4 console.log(f.__proto__ === fn.prototype);  // true

 從上面的代碼,應該就能夠明白這兩者的關係了,若是能理解這一點,接下來就能夠開始分析繼承的實現原理了。prototype

開頭說過,繼承是基於原型鏈實現的,那麼什麼是原型鏈呢?首先咱們看幾個例子:設計

 1 function fn() { }
 2 
 3 // 首先記住,一個對象的__proto__指向它的構造函數的原型對象
 4 var f = new fn();
 5 console.log(f instanceof Object);   // true,說明此時f是一個Object
 6 console.log(f.__proto__ === fn.prototype);   // true,沒毛病,由於f的構造函數就是fn
 7 var obj = fn.prototype; // 咱們看看fn的原型對象是什麼類型?確定是對象!
 8 console.log(obj.__proto__ === Object.prototype);    // true,那對象就是Object,它的構造函數就是Object
 9 obj = Object.prototype; // 那Object的原型對象應該也是個對象吧
10 console.log(obj.__proto__ === null);    // true,爲何是null?這是JavaScript設計的,由於若是不是null,就會無限循環

以上的例子說明了f的構造函數的原型,f的構造函數的原型的原型,f的構造函數的原型的原型的原型,用圖形表示就是:code

f.__proto__ ---> f.__proto__.__proto__ ---> f.__proto__.__proto__.__proto__

那麼上面這條「鏈」就是咱們所說的原型鏈了!這個過程理解了能夠繼續往下。對象

那麼原型鏈是如何實現繼承的?在JavaScript中,你對一個對象調用一個方法或者獲取一個屬性,它就會自動的在原型鏈上面尋找,一直到找到或者原型對象爲null。

 1 // 再舉個例子
 2 function fn() {}
 3 
 4 var f = new fn();
 5 
 6 // 此時fn並無方法toString
 7 console.log(f.toString()); // 輸出[object Object]
 8 // 爲何?
 9 // 按照剛剛分析的原型鏈,它會如今fn.prototype中尋找
10 console.log(fn.prototype);  // fn {},沒有
11 // 再在fn.prototype.__proto__(Object.prototype)中尋找
12 console.log(fn.prototype.__proto__);

上面這個例子說明,f的 toString 方法實際上是從 Object.prototype 繼承而來的。

若是上面這些都能明白,那咱們就能夠本身實現繼承了。

 1 // 回到前面的例子
 2 function SuperClass() {
 3     this.time = new Date().toLocaleString();
 4 }
 5 
 6 // // 添加一個方法
 7 SuperClass.prototype.sayHello = function() {
 8     console.log('hello');
 9 };
10 
11 // // 添加一個屬性
12 SuperClass.prototype.name = 'super';
13 
14 // 定義一個子類繼承SuperClass
15 function SubClass() {
16     // 在子類的構造函數中調用父類的構造函數
17     SuperClass.call(this);
18 }
19 
20 SubClass.prototype = Object.create(SuperClass.prototype);  // 繼承父類的屬性和方法
21 // SubClass.prototype = SuperClass.prototype;  // 不能直接賦值!由於JavaScript中的對象賦值都是淺複製,有反作用,也就是說修改子類的原型也會修改父類
22 // SubClass.prototype = new SuperClass();   // 也不要這樣作,由於這樣會實例化一個SuperClass,假如SuperClass的構造函數中定義了一個很大的對象,就會形成內存浪費!
23 SubClass.constructor = SubClass;    // 上一行代碼會把子類的構造函數給覆蓋了,這裏把它恢復了。注意:constructor會影響instanceof運算符的結果
24 
25 SubClass.prototype.subfn = function() {
26     console.log('this is a sub func');
27 }
28 
29 var sc = new SubClass();
30 console.log(sc.time);   // 2018-12-18 22:02:09
31 sc.sayHello();  // hello
32 sc.subfn(); // this is a sub func

 上面就是一個簡單的繼承,要實現多重繼承也是相似的:

 1 // 在上面的代碼修改
 2 // 另外一個父類
 3 function SuperClassB() {}
 4 SuperClassB.prototype.anotherfn = function() {
 5     console.log('another super');
 6 }
 7 
 8 // ...
 9 
10 // 定義一個子類繼承SuperClass
11 function SubClass() {
12     // 在子類的構造函數中調用父類的構造函數
13     SuperClass.call(this);
14     SuperClassB.call(this); // 添加
15 }
16 
17 var prototype = Object.create(SuperClass.prototype);  // 繼承父類的屬性和方法
18 prototype = Object.assign(prototype, SuperClassB.prototype);  // 合併兩個父類的原型對象
19 SubClass.prototype = prototype  // 繼承父類的屬性和方法
20 
21 // ...
22 
23 sc.anotherfn(); // another super
相關文章
相關標籤/搜索