
前言
我們都知道,js在ES6以前是沒有類的概念的,如果想用面向對象的思想來開發它就要去了解他的繼承實作方式。
JavaScript是一門基于原型繼承的語言,這在語言設計之初就已經被确定了的,即使在後來的ES6中加入了類的概念,加入了extends關鍵字,但仍舊無法改變他作為原型繼承的本質,ES6的繼承可以看作是ES5繼承方式的文法糖,将之前的寫好的繼承方式合法化,規範化。
什麼是原型
我一般喜歡把原型比作模型,或者說模具更加貼切。比如說一個水杯,他是圓的還是方的就是由這個模具決定的,如果這個模具改變了樣子,那麼生産出來的水杯也會變了樣子。
這樣來描述,你可能會覺得,叫什麼原型,叫構造器不是更貼切。沒錯,在這個例子中,模具就是水杯的構造器(constructor)。
那麼原型和說的這個有什麼關系呢。假設我們的這個水杯是有額外功能的,加熱和制冷。那麼問題就來了,構造器隻能決定水杯的樣子,是沒辦法給他提供加熱和制冷的功能的。這個時候就需要原型(prototype)了。
一般情況,構造器決定樣子,名稱等固定的屬性。原型決定的是功能,可以進行操作的方法。構造器(constructor)和原型(prototype)共同決定了一個物體的存在形式。
構造器(constructor)和原型(prototype)的關系怎麼來描述呢
原型對象的 constructor 是 構造器。
構造器的 prototype 是原型對象。
在js中有一句話叫萬物皆對象,每個對象都有原型。我們建立函數,如果采用new的方式調用,當然這種調用方式有個名字叫執行個體化。
// 建立一個函數
function B(name) {
this.name = name;
};
// 執行個體化
var bb = new B('執行個體化的b');
console.log(bb.name); // 執行個體化的b;
如上面的代碼,bb是通過B執行個體化之後得到的對象。在這裡B就是一個構造器,他所擁有的名字(this.name)屬性會帶給bb;這也符合之前杯子的例子,杯子的屬性會從構造器中獲得。
假如我們想要做出來的bb具有一定的功能,那麼就需要在原型上下功夫了。根據上面構造器和原型的關系。我們可以這樣做。
// 建立一個函數
function B(name) {
this.name = name;
};
// 在原型上添加一個方法
B.prototype.tan = function() {
alert('彈出框');
}
// 執行個體化
var bb = new B('執行個體化的b');
console.log(bb.name); // 執行個體化的b;
bb.tan(); // alert('彈出框');
在上面的代碼中,我們在B的原型上添加了一個tan的方法,在執行個體化出來的bb也具備了這個方法。這裡我們就簡單實作了一個類。
用下面一張圖,說明一下。執行個體對象(bb), 原型(prototype), 構造函數(constructor)的關系。
B是我們構造的一個類,這裡稱為構造函數。他用prototype指向了自己的原型。而他的原型也通過constructor指向了它。
B.prototype.constructor === B; // true;
bb和B沒有直接的關聯,雖然B是bb的構造函數,這裡用虛線表示。bb有一個__ proto__屬性,指向了B的prototype
bb.__ proto__ === B.prototype; // true;
bb.__ proto__.constructor = B; // true;
總之
1,每建立一個函數B,就會為該函數建立一個prototype屬性,這個屬性指向函數的原型對象;
2,原型對象會預設去取得constructor屬性,指向構造函數。
3,當調用構造函數建立一個新執行個體bb後,該執行個體的内部将包含一個指針__ proto__,指向構造函數的原型對象。
預設原型
我們知道,所有引用對象都預設繼承了Object,所有函數的預設原型都是Object的執行個體。
之前說過構造函數和原型之間具備對應關系,如下:
既然函數的預設原型都是Object的執行個體,B的原型對象也應該是Object的執行個體子,也就是說。B的原型的__ proto__應該指向Objct的原型。
Object的原型對象的原型是最底部了,是以不存在原型,指向NULL;
console.log(Object.prototype.__ proto__); // null;
Function對象
我們知道,函數也是對象,任何函數都可以看作是由構造函數Function執行個體化的對象,是以Function與其原型對象之間也存在如下關系
如果将Foo函數看作執行個體對象的話,其構造函數就是Function(),原型對象自然就是Function的原型對象;同樣Object函數看作執行個體對象的話,其構造函數就是Function(),原型對象自然也是Function的原型對象。
如果Function的原型對象看作執行個體對象的話,如前所述所有對象都可看作是Object的執行個體化對象,是以Function的原型對象的__ proto __指向Object的原型對象。
到這裡prototype,__ proto __, constructor三者之間的關系我們就說完了。
實作繼承
function Animal() {
this.type = '動物';
}
Animate.prototype.eat = function() {
console.log('吃食物');
}
上面定義了一個動物類,作為父類。
function Cat(name) {
this.name = name || ‘小貓’;
}
定義了一個貓作為子類,這裡我們要繼承動物類的eat方法和type屬性
function Cat(name){
Animal.call(this);
this.name = name || '小貓';
}
在執行個體化Cat時通過call執行了Animal類, 這樣Animal中的this就被修改為目前Cat的this。所有的屬性也會加在Cat上。
(function(){
// 建立一個沒有執行個體方法的類
var Super = function(){};
Super.prototype = Animal.prototype;
//将執行個體作為子類的原型
Cat.prototype = new Super();
})();
通過寄生方式,砍掉父類的執行個體屬性,這樣,在調用兩次父類的構造的時候,就不會初始化執行個體方法/屬性。而父類的方法仍舊可以指派給子類。
Cat.prototype = new Super(); 可以實作方法的繼承,是因為,根據前面的知識我們知道 new Cat()的__ proto __是指向 Cat的原型的。
(new Cat()).__ proto __ === Cat.prototype; // true
new Cat()所有的方法都是從原型上取到的。
我們通過 Cat.prototype = new Super(); 公式變成了。
(new Cat()).__ proto __ = Cat.prototype = new Super();
是以現在(new Cat()).__ proto __ 指向了 Super的prototype。也就是new Cat的方法是繼承自Super.prototype。
Super.prototype又在前一句等于Animal.prototype。是以實作了Cat繼承Animal。
這裡我們就實作了js屬性和方法的繼承。不過還在最後一個小問題。
我們知道 prototype 和 constructor 是互相指向的。
Cat.prototype.constructor 應該等于 Cat;
但是随着我們的修改了Cat.prototype = Super.prototype;
現在Cat.prototype.constructor是等于Super的。
是以我們還應該糾正這個問題,一句話搞定。
Cat.prototype.constructor = Cat; // 需要修複下構造函數
以上就是js的原型繼承,完整代碼如下。
// 建立一個父類
function Animal() {
this.type = '動物';
}
// 給父類添加一個方法
Animate.prototype.eat = function() {
console.log('吃食物');
}
// 建立一個子類
function Cat(name){
// 繼承Animal的屬性
Animal.call(this);
this.name = name || '小貓';
}
// 繼承 Animal 的方法
(function(){
// 建立一個沒有執行個體方法的類
var Super = function(){};
Super.prototype = Animal.prototype;
//将執行個體作為子類的原型
Cat.prototype = new Super();
})();
// 修正構造函數
Cat.prototype.constructor = Cat;
好啦,js的繼承原理和prototype,proto, constructor之間的關系我們就說完了,ES6底層的實作方式原理基本相同。
學習更多技能
請點選下方公衆号