再說說__proto__和prototype以及js的繼承

1.proto和prototype

JS中的原型鏈已經是一個老生常談的問題,畢竟也是JS 這門語言的特色之一了。

這裏寫圖片描述
首先“萬物皆對象“,雖然這句話一直有爭議,但是有它的道理的,null類型這些的爭論這裏就不說了。
對象中有個屬性proto,被稱爲隱式原型,這個隱式原型指向構造改對象的構造函數的原型,這也保證了實例能夠訪問在構造函數原型中定義的屬性和方法。這個實例可能是如圖中的new Foo()出來的實例。

構造該對象的f1,f2構造函數是fuction Foo(),它的原型是Foo.prototype,那麼f1,f2就指向了構造該對象的構造函數的原型,也就是Foo.prototype,那麼構造函數Foo()它的proto指向哪裏了,還是找它的構造函數,它的構造函數是Function(),那麼它的proto就指向了Fuction.prototype,沿着proto這條路最上就是Object.prototype,Object的proto就是null了。

剛剛說的對象有個proto屬性,方法也是對象,方法中除了有proto之外(這個proto指向構造該函數/對象的構造原型,也就是上一層了),還有prototype,這個屬性就是原型屬性,他是一個指針,指向一個對象,這個對象就叫原型對象,這裏放着包含所有實例共享的屬性和方法,這個原型對象裏面有一個屬性constructor,這個屬性也包含一個指針,指回了原構造函數。

1.構造函數Foo()構造函數的原型屬性Foo.prototype指向了原型對象,在原型對象裏有共有的方法,所有構造函數聲明的實例(這裏是f1,f2)都可以共享這個方法。

2.原型對象Foo.prototypeFoo.prototype保存着實例共享的方法,有一個指針constructor指回構造函數。

3.實例f1和f2是Foo這個對象的兩個實例,這兩個對象也有屬性proto,指向構造函數的原型對象,這樣子就可以像上面1所說的訪問原型對象的所有方法。

4.構造函數Foo()除了是方法,也是對象,它也有proto屬性,指向誰呢?指向它的構造函數的原型對象。函數的構造函數不就是Function嘛,因此這裏的proto指向了Function.prototype。其實除了Foo(),Function(), Object()也是一樣的道理。原型對象也是對象,它的proto屬性,又指向誰呢?同理,指向它的構造函數的原型對象。這裏是Object.prototype.最後,Object.prototype的proto屬性指向null。

5.對象有屬性proto,指向該對象的構造函數的原型對象。
方法除了有屬性proto,還有屬性prototype,prototype指向該方法的原型對象。

6.再看圖。

2.繼承

1.父類的實例作爲子類的原型

function Animal(name) {
  // 屬性
  this.name =  name || "Animal";
  // 實例方法
  this.sleep = function() {
    console.log(this.name + '正在睡覺!');
  }
}

// 原型方法
Animal.prototype.eat = function(food) {
  console.log(this.name + '正在吃' + food);
};


function Cat() { 
}
Cat.prototype = new Animal();
Cat.prototype.name = 'cat';

var cat = new Cat();
console.log(cat.name);
console.log(cat.eat('fish'));
console.log(cat.sleep());
console.log(cat instanceof Animal); //true 
console.log(cat instanceof Cat); //true

cat._proto_ === Cat.prototype //true

構造cat對象的構造函數是Cat(),它的原型是Cat.prototype,那麼隱式原型proto就指向構造該對象的構造函數的原型對象,那麼cat的proto就指向的是Cat.prototype。

Cat.prototype._proto_ === Animal.prototype //true
原型對象也是對象,是對象就有proto,Animal的實例返回給了這個原型對象,那麼這個原型對象的隱式原型proto就指向的是構造該對象的構造函數的原型,我們看看這個原型對象的構造函數是誰
這裏寫圖片描述

那麼Animal()構造函數的原型對象就是Animal.prototype了。自然就有上面true的結果了。

這種方法的繼承的缺點:
1. 父類的引用屬性和原型對象的引用屬性是所有實例共享的
2. 創建子類實例時,無法向父類構造函數傳參
3. 不能多繼承
第一個致命缺點,因爲我們每個實例各自的屬性互不干擾纔對:
這裏寫圖片描述

注意原型上的方法/屬性是共享的
這裏寫圖片描述

2.構造繼承

沒有用到原型,使用父類的構造函數來增強子類實例,等於直接是複製父類的實例屬性給子類。

經典繼承也叫做 “借用構造函數” 或 “僞造對象” 。其基本思想是:在子類型構造函數的內部調用超類型構造函數。函數只不過是在特定環境中執行代碼的對象,因此可以通過使用apply() 和call() 方法也可以在新創建的對象上執行構造函數。(JS高程)

function Cat(name){
  Animal.call(this);
  this.name = name || 'Tom';
}

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true

特點:

解決了1中,子類實例共享父類引用屬性的問題
創建子類實例時,可以向父類傳遞參數(通過call的後面參數)
可以實現多繼承(call多個父類對象)

缺點:

實例並不是父類的實例,只是子類的實例
只能繼承父類的實例屬性和方法,不能繼承原型屬性/方法
無法實現函數複用,每個子類都有父類實例函數的副本,影響性能

3.組合繼承

通過調用父類構造,繼承父類的屬性並保留傳參的優點,然後通過將父類實例作爲子類原型,實現函數複用

function Cat(name){
  Animal.call(this);
  this.name = name || 'Tom';
}
Cat.prototype = new Animal();

//組合繼承也是需要修復構造函數指向的。

Cat.prototype.constructor = Cat;

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // true

這種方式看似是原型繼承和構造繼承的組合,彌補了構造繼承只能繼承實例屬性/方法,不能繼承原型屬性/方法的缺點,也彌補了原型繼承引用屬性共享的問題,可向父類傳參,函數可複用,即是子類的實例,也是父類的實例。

缺點就是調用了兩次父類構造函數,生成了兩份實例(子類實例將子類原型上的那份屏蔽了)
這裏寫圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章