在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