在講原型關係以前給咱們來看一張圖片:java
由圖咱們可知幾個關係:segmentfault
若是試圖引用對象(實例instance)的某個屬性,會首先在對象內部尋找該屬性,直至找不到,而後纔在該對象的原型(instance.prototype)裏去找這個屬性.若是還找不到則往原型的原型上找,這樣一個層層查找造成的一個鏈式的關係被稱爲原型鏈。 如圖: 數組
爲了解釋這個過程,用下面的例子作下說明:function Father(){
this.property = true;
}
Father.prototype.getFatherValue = function(){
return this.property;
}
function Son(){
this.sonProperty = false;
}
//繼承 Father
Son.prototype = new Father();//Son.prototype被重寫,致使Son.prototype.constructor也一同被重寫
Son.prototype.getSonVaule = function(){
return this.sonProperty;
}
var instance = new Son();
console.log(instance.getFatherValue());//true
複製代碼
可見son實例對象找不到getFatherValue方法,只能前去Father原型那裏去找,返回值爲true。 若是,對子類son進行改造:bash
function Father(){
this.property = true;
}
Father.prototype.getFatherValue = function(){
return this.property;
}
function Son(){
this.sonProperty = false;
this.getFatherValue = function(){
return this.sonProperty;
}
}
//繼承 Father
Son.prototype = new Father();//Son.prototype被重寫,致使Son.prototype.constructor也一同被重寫
Son.prototype.getSonVaule = function(){
return this.sonProperty;
}
var instance = new Son();
console.log(instance.getFatherValue());//false
複製代碼
你會發現當子類裏出現相同的方法時,則執行子類中的方法,也就驗證了以前的實例對象查找引用屬性的過程。函數
使用原型鏈後, 咱們怎麼去判斷原型和實例的這種繼承關係呢? 方法通常有兩種.post
第一種是使用 instanceof 操做符, 只要用這個操做符來測試實例(instance)與原型鏈中出現過的構造函數,結果就會返回true. 如下幾行代碼就說明了這點.測試
console.log(instance instanceof Object);//true
console.log(instance instanceof Father);//true
console.log(instance instanceof Son);//true
複製代碼
因爲原型鏈的關係, 咱們能夠說instance 是 Object, Father 或 Son中任何一個類型的實例. 所以, 這三個構造函數的結果都返回了true.優化
第二種是使用 isPrototypeOf() 方法, 一樣只要是原型鏈中出現過的原型,isPrototypeOf() 方法就會返回true, 以下所示.ui
console.log(Object.prototype.isPrototypeOf(instance));//true
console.log(Father.prototype.isPrototypeOf(instance));//true
console.log(Son.prototype.isPrototypeOf(instance));//true
複製代碼
原型鏈並不是十分完美, 它包含以下兩個問題:this
爲此,下面將有一些嘗試以彌補原型鏈的不足.
爲解決原型鏈中上述兩個問題, 咱們開始使用一種叫作借用構造函數(constructor stealing)的技術(也叫經典繼承).
基本思路:就是在子類的構造函數裏調用父類的構造函數。
function Father(){
this.colors = ["red","blue","green"];
function hello() {
console.log('hello world')
}
}
function Son(){
Father.call(this);//繼承了Father,且向父類型傳遞參數
}
var instance1 = new Son();
instance1.colors.push("black");
console.log(instance1.colors);//"red,blue,green,black"
var instance2 = new Son();
console.log(instance2.colors);//"red,blue,green" 可見引用類型值是獨立的
複製代碼
考慮此,借用構造函數的技術也不多單獨使用.
該方法最初由道格拉斯·克羅克福德於2006年在一篇題爲 《Prototypal Inheritance in JavaScript》(JavaScript中的原型式繼承) 的文章中提出. 他的想法是藉助原型能夠基於已有的對象建立新對象, 同時還沒必要所以建立自定義類型. 大意以下:
基本思路:在create()函數內部, 先建立一個臨時性的構造函數, 而後將傳入的對象做爲這個構造函數的原型,最後返回了這個臨時類型的一個新實例.
function create(o){
function Fn() {}
Fn.prototype = o;
return new Fn();
}
複製代碼
實質上就是對傳入的實例o進行了一次淺拷貝。
function Father(){
this.colors = ["red","blue","green"];
}
let fa = new Father()
var instance1 =create(fa);
instance1.colors.push("black");
console.log(instance1.colors); // [ 'red', 'blue', 'green', 'black' ]
var instance2 = create(fa);
instance2.colors.push("white");
console.log(instance2.colors); //[ 'red', 'blue', 'green', 'black', 'white' ]
複製代碼
在此例中:instance1與instance的原型是同一個對象,當instance1操做原型的引用類型數值,也會影響到instance2。此時數據是共享的。 再看下面這個例子:
function Father(){
this.colors = ["red","blue","green"];
}
var instance1 = create(new Father());
instance1.colors.push("black");
console.log(instance1.colors); // [ 'red', 'blue', 'green', 'black' ]
var instance2 = create(new Father());
instance2.colors.push("white");
console.log(instance2.colors); // [ 'red', 'blue', 'green', 'white' ]
複製代碼
此時因爲原型實例不是同一個,數據不在共享。
在 ECMAScript5 中,經過新增 object.create() 方法規範化了上面的原型式繼承. object.create() 接收兩個參數:
關鍵點:原型式繼承中, 包含引用類型值的屬性始終都會共享相應的值, 就像使用原型模式同樣.
組合繼承, 有時候也叫作僞經典繼承,指的是將原型鏈和借用構造函數的技術組合到一塊,從而發揮二者之長的一種繼承模式。
基本思路:使用原型鏈實現對原型屬性和方法的繼承,經過借用構造函數來實現對實例屬性的繼承.
以下例:
function Father(name){
this.name = name;
this.colors = ["red","blue","green"];
}
Father.prototype.sayName = function(){
alert(this.name);
};
function Son(name,age){
Father.call(this,name);//繼承實例屬性,第一次調用Father()
this.age = age;
}
Son.prototype = new Father();//繼承父類方法,第二次調用Father()
Son.prototype.sayAge = function(){
alert(this.age);
}
var instance1 = new Son("louis",5);
instance1.colors.push("black");
console.log(instance1.colors);//"red,blue,green,black"
instance1.sayName();//louis
instance1.sayAge();//5
var instance1 = new Son("zhai",10);
console.log(instance1.colors);//"red,blue,green"
instance1.sayName();//zhai
instance1.sayAge();//10
複製代碼
在這個例子中,類Son經過構造函數繼承能夠向父類Father傳參,同時可以保證明例數據不被共享。同時經過原型繼承能夠複用父類的方法,兩繼承組合起來,各取所需。
組合繼承避免了原型鏈和借用構造函數的缺陷,融合了它們的優勢,成爲 JavaScript 中最經常使用的繼承模式. 並且, instanceof 和 isPrototypeOf( )也能用於識別基於組合繼承建立的對象.
此處調用了兩次父類的構造函數,後面的寄生式組合繼承將會對這個問題進行優化。
寄生式繼承是與原型式繼承緊密相關的一種思路。 基本思路:寄生式繼承的思路與(寄生)構造函數和工廠模式相似, 即建立一個僅用於封裝繼承過程的函數,該函數在內部以某種方式來加強對象,最後再像真的是它作了全部工做同樣返回對象. 以下.
function createAnother(original){
var clone = create(original);//經過調用create函數建立一個新對象
clone.sayHi = function(){//以某種方式來加強這個對象
alert("hi");
};
return clone;//返回這個對象
}
複製代碼
直白點,所謂寄生式繼承也就是在其餘繼承方式(構造繼承、原型繼承等)上增長新的功能,返回新的對象。
前面講過,組合繼承是 JavaScript 最經常使用的繼承模式; 不過, 它也有本身的不足. 組合繼承最大的問題就是不管什麼狀況下,都會調用兩次父類構造函數: 一次是在建立子類型原型的時候, 另外一次是在子類型構造函數內部. 寄生組合式繼承就是爲了下降調用父類構造函數的開銷而出現的 .以下例:
function extend(subClass,superClass){
var prototype = create(superClass.prototype);//建立對象
prototype.constructor = subClass;//加強對象
subClass.prototype = prototype;//指定對象
}
複製代碼
下面咱們來看下extend的另外一種更爲有效的擴展.
// 把上面的 create 拆開,其實差很少。
function extend(subClass, superClass) {
var F = function() {};
F.prototype = superClass.prototype;
subClass.prototype = new F();
subClass.prototype.constructor = subClass;
subClass.superclass = superClass.prototype;
if(superClass.prototype.constructor == Object.prototype.constructor) {
superClass.prototype.constructor = superClass;
}
}
複製代碼
console.log(instance1.hasOwnProperty('age'));//true
複製代碼
console.log(Father.prototype.isPrototypeOf(instance1));//true
複製代碼
instanceof 運算符是用來在運行時指出對象是不是構造器的一個實例, 例如漏寫了new運算符去調用某個構造器, 此時構造器內部能夠經過 instanceof 來判斷.(java中功能相似)
function f(){
if(this instanceof arguments.callee)
console.log('此處做爲構造函數被調用');
else
console.log('此處做爲普通函數被調用');
}
f();//此處做爲普通函數被調用
new f();//此處做爲構造函數被調用
複製代碼
new實質上作了三件事;
var obj = {};
obj.__proto__ = F.prototype;
F.call(obj); //執行
複製代碼
第一行,咱們建立了一個空對象obj; 第二行,咱們將這個空對象的__proto__成員指向了F函數對象prototype成員對象; 第三行,咱們將F函數對象的this指針替換成obj,而後再調用F函數.
咱們能夠這麼理解: 以 new 操做符調用構造函數的時候,函數內部實際上發生如下變化:
在繼承關係裏,內部屬性數值變不變,數據共不共享前面也有所介紹,可是不夠具體。這塊時常使人迷惑,決定單獨拿出來說講:
首先在繼承關係裏,原型繼承與構造函數繼承能夠分紅兩個比較重要的繼承關係,其餘的繼承都是在這基礎上演變組合出來的,因此搞懂這兩個繼承關係中的數據變化,就差很少了。
在講區別以前咱們先兩個例子:
function kk() {
this.a = 3;
this.k = {l: 5};
}
function j() {
kk.call(this)
}
let m = new j();
m.a = 9;
m.k.l = 9;
let n = new j();
console.log(n.a, n.k.l);
複製代碼
打印結果:
可見,原型kk的數據並無改變,再看一個例子:function kk() {
this.a = 3;
this.k = {l: 5};
}
function j() {
}
j.prototype = new kk();
let m = new j();
m.a = 9;
m.k.l = 9;
let n = new j();
console.log(n.a, n.k.l);
複製代碼
打印結果:
你會發現:原型裏a沒變, k 變了。 對比上例,a始終沒變,k有所區別,到底是什麼緣由呢?若是你的眼睛足夠雪亮,會一眼看出上例是構造函數繼承,下例是原型繼承,它兩的區別以前已經說過,構造函數繼承數據不會共享,而原型繼承會共享。因而你會說爲何a怎麼不變,你又在忽悠人,哼!哈哈哈,抱歉,有沒有看見a是基本數據類型,k是引用類型(引用類型包括:對象、數組、函數。)啊,基本數據類型是指針的指向區別,引用類型是地址的指向區別。不瞭解這塊能夠看看這篇文章:segmentfault.com/a/119000000…
使用權威指南6.2.2繼承那塊的一句話「若是容許屬性賦值操做,它也老是在原始對象上創造屬性或者對已有屬性賦值,而不會修改原型鏈,在JavaScript裏,只有查詢屬性才能感覺到繼承的存在,而設置屬性則與繼承無關」。
如何理解這句話?我想是指繼承關係中屬性在自己內部找不到的時候纔會去原型裏找,只是借用屬性,可是並不會修改原型自己的屬性值,這也就解釋了基本數據類型始終不變的緣由。而原型繼承中因爲使用的同一原型對象,裏面的引用類型使用同一個地址,致使應用類型的數值是能夠變化的。
總結兩點:
參考地址: juejin.im/post/58f94c…