天天看點

JavaScript 的原型鍊

JavaScript 的原型鍊

前言

我們都知道,js在ES6以前是沒有類的概念的,如果想用面向對象的思想來開發它就要去了解他的繼承實作方式。

JavaScript是一門基于原型繼承的語言,這在語言設計之初就已經被确定了的,即使在後來的ES6中加入了類的概念,加入了extends關鍵字,但仍舊無法改變他作為原型繼承的本質,ES6的繼承可以看作是ES5繼承方式的文法糖,将之前的寫好的繼承方式合法化,規範化。

什麼是原型

我一般喜歡把原型比作模型,或者說模具更加貼切。比如說一個水杯,他是圓的還是方的就是由這個模具決定的,如果這個模具改變了樣子,那麼生産出來的水杯也會變了樣子。

這樣來描述,你可能會覺得,叫什麼原型,叫構造器不是更貼切。沒錯,在這個例子中,模具就是水杯的構造器(constructor)。

那麼原型和說的這個有什麼關系呢。假設我們的這個水杯是有額外功能的,加熱和制冷。那麼問題就來了,構造器隻能決定水杯的樣子,是沒辦法給他提供加熱和制冷的功能的。這個時候就需要原型(prototype)了。

一般情況,構造器決定樣子,名稱等固定的屬性。原型決定的是功能,可以進行操作的方法。構造器(constructor)和原型(prototype)共同決定了一個物體的存在形式。

構造器(constructor)和原型(prototype)的關系怎麼來描述呢

原型對象的 constructor 是 構造器。

構造器的 prototype 是原型對象。

JavaScript 的原型鍊

在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)的關系。

JavaScript 的原型鍊

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的執行個體。

之前說過構造函數和原型之間具備對應關系,如下:

JavaScript 的原型鍊

既然函數的預設原型都是Object的執行個體,B的原型對象也應該是Object的執行個體子,也就是說。B的原型的__ proto__應該指向Objct的原型。

JavaScript 的原型鍊

Object的原型對象的原型是最底部了,是以不存在原型,指向NULL;

console.log(Object.prototype.__ proto__); // null;      
JavaScript 的原型鍊

Function對象

我們知道,函數也是對象,任何函數都可以看作是由構造函數Function執行個體化的對象,是以Function與其原型對象之間也存在如下關系

JavaScript 的原型鍊

如果将Foo函數看作執行個體對象的話,其構造函數就是Function(),原型對象自然就是Function的原型對象;同樣Object函數看作執行個體對象的話,其構造函數就是Function(),原型對象自然也是Function的原型對象。

JavaScript 的原型鍊

如果Function的原型對象看作執行個體對象的話,如前所述所有對象都可看作是Object的執行個體化對象,是以Function的原型對象的__ proto __指向Object的原型對象。

JavaScript 的原型鍊

到這裡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底層的實作方式原理基本相同。

學習更多技能

請點選下方公衆号

繼續閱讀