再談JavaScript面向對象思想及繼承

前言

爲何說是再談呢,網上講解這個的博客的不少,我開始學習也是看過,敲過就沒了,自覺得理解了就結束了,書到用時方恨少啊。實際開發中一用就打磕巴,因而在從新學習了以後分享出來。開了這麼一個宏觀的題目,須要作一下簡單說明,這篇文章將會講解如下幾個問題:java

  1. 什麼是面向對象編程思想,爲何要用面向對象思想。
  2. js中的面向對象思想和其餘靜態語言相比有什麼不一樣。
  3. js中prototype,constructor,__proto__這些都是什麼鬼?怎麼用他們實現面向對象中的繼承。
  4. 一些小感悟小建議之類的吧啦吧啦。

下面咱們直接開始乾貨。。。node

面向對象 what's it? why ues it?

什麼是面向對象編程?看成者剛開始工做時,懷着對面向對象編程的無限敬仰和好奇,問了同事Java大牛這個問題,他的回答引我深思:不要面向對象編程,要面向工資編程。言歸正傳,面向對象中的對象,固然不是男女友的對象,ECMAScript中,對象是一個無序屬性集,這裏的「屬性」能夠是基本值、另外一個對象或者函數。實際應用能夠理解爲一本書,一我的,一個班級,因此萬物都是對象。對象的屬性怎麼理解,以人爲例,指人的名字、身高、體重等等,對象的屬性還能夠是函數稱之爲方法,指代對象的一些操做,動做。如人的說話,走路等等。提到面向對象,那就須要提到面向過程,咱們不用官方的方式來解釋,從實際問題中思考。編程

假設如今項目需求爲畫一個三角形,一個矩形。直接編寫代碼時,咱們確定考慮的是第一步 畫三角形 第二步 畫矩形。咱們會編寫一個三角形函數triangle() 一個矩形函數rect() 而後一步步調用,這是面向過程的思想。設計模式

function triangle() {...}
function rect() {...}

triangle();
rect();

面向對象中咱們首先會抽象問題,矩形三角形都是對象,他們的類型都是形狀。他們有各自的邊長頂點,那麼咱們會先建立一個基本對象 形狀 Shape 屬性有頂點、邊長,三角形Triangle和矩形Rect都是基本對象擴展出的新對象,有各自的畫圖方法draw(),而後用對象獲得具體的指向對象(即實例,後文解釋)triangle調用draw方法數組

function Shape() {...}
function Triangle() {...}
function Rect() {...}

let triangle = new Triang();
triangle.draw();
let rect = new Rect();
rect.draw();

面對一個問題,面向過程的思路是第一步作什麼,第二步作什麼 面向對象則須要先分析出問題中的對象都有什麼,對象的屬性、方法是什麼,讓對象要作什麼。
假設如今須要得到畫出矩形的邊長,面向對象中只須要在Rect中加上一個方法就能夠,面向過程則須要拿到畫出的矩形,再獲得邊長,相比較而言面向對象易於擴展。瀏覽器

面向對象中有三大特徵,封裝,繼承,多態。封裝指將變化封裝起來,外面調用時不須要知道內部的實現,繼承指的是一個對象能夠共享父級對象的一些屬性,好比上文的問題中,形狀Shape有頂點這個屬性,三角形和矩形均可以繼承該屬性而不須要再從新定義。多態指的是封裝以後的變化如何處理,好比上文中將draw函數放在形狀Shape中,內部實現就是鏈接點,三角形和矩形調用父級對象的draw,三角形與矩形的頂點不一樣。app

爲何要使用面向對象?面向對象由於封裝,繼承,多態的特徵使程序更易於擴展,維護,重用。好比在另一個環境中咱們須要畫三角形,咱們只須要將三角形這個對象及形狀父級對象引入,剩下關於三角形的操做都是三角形這個對象的內部實現。維護起來去該對象的該方法找錯,比在整個環境中找三角形函數要好不少。編程語言

js中的面向對象

面向對象中類指的是同一類型對象的抽象,首字母大寫,好比上文中的形狀 Shape 類,三角形是經過Shape擴展而來,則也是一個類,Shape稱之爲它的父類,它是Shape的子類,同理 Rect也是Shape的一個子類。類的具體抽象稱之爲實例,一般爲小寫,建立實例的過程稱之爲實例化。上文中triangle就是一個Triangle三角形的實例,指具體畫出的那個三角形。關於父類,子類,實例咱們再用一個一個示例來展現函數

父類 Animal 
子類 Cat 實例 cat1_tom
子類 Dog 實例 dog1

Animal 指全部動物,Cat 指全部貓 繼承Animal 是動物的一個子類,cat1_tom 指的具體一個叫 tom 的貓。有了類咱們就須要給類加一些標識,以區分類之間的區別、即屬性和方法。工具

1.走出‘類’,走進原型

當咱們弄清楚了類是什麼,JavaScript沒有類的概念,是經過原型來實現面向對象。在以類爲中心的面向對象編程語言中,類和對象的關係能夠想象成鑄模和鑄件的關係,對象老是從類中建立而來。而在原型編程的思想中,類並非必需的,對象未必須要從類中建立而來,一個對象是經過克隆另一個對象所獲得的。

從設計模式的角度講,原型模式是用於建立對象的一種模式,若是咱們想要建立一個對象,一種方法是先指定它的類型,而後經過類來建立這個對象。原型模式選擇了另一種方式,咱們再也不關心對象的具體類型,而是找到一個對象,而後經過克隆來建立一個如出一轍的對象。而克隆出來的這個對象會記住他的原型,由誰克隆而來,同時也會共享原型的屬性和方法。這樣一個一個對象克隆而來,則造成了一條原型鏈。對上文中的例子而言,三角形的原型是形狀,貓和狗的原型是動物。

2.構造函數

在java中new Class()new 以後跟的是一個類名,而在js中類以後跟的是一個構造函數。

function Shape(name) {
  this.val = 1;
  this.name = name;
  this.all = '圖形';
  return this.name
}
let a = Shape('a'); // 'a'

let shape1 = new Shape('triangle'); 
let shape2 = new Shape('rect');

構造函數的定義與通常函數的定義相同,注意首字母大寫。構造函數本質上仍是一個函數,能夠傳參能夠有返回值,只是內部使用了this變量,函數存在調用問題:

  1. 直接調用:在瀏覽器環境中至關於在window上掛在了val這個屬性,值爲1。請注意這個特色,若是Shape.call(obj) 即至關於設定obj對象的val爲1。
  2. new 調用:生成一個實例,即生成一個新對象,這個this指向當前新生成的對象。

constructor和prototype

這裏的概念還但願你們閱讀緩慢 最好能在瀏覽器或者node環境下敲一下理解更深。請首先必定理解何爲實例何爲構造函數(構造器)。他們的關係是
__A爲B的構造函數 則 B爲A的一個實例__。

在山的那邊,海的那邊,有一個prototype ,還有一個__proto__

首先建立一個Cat的構造函數,但願say是Cat的實例共享屬性,

function Cat(name) {
  this.name = name;
  this.say = function() {console.log(this.name)};
}

let cat1 = new Cat('tom'); 
let cat2 = new Cat('bob');
cat1.say === cat2.say // false

可是發現cat1 cat2的共有方法all並無共享,每個實例對象,都有本身的屬性和方法的副本。這不只沒法作到數據共享,也是極大的資源浪費, 那麼引入prototype對象:

function Cat(name) {
  this.name = name;
}
Cat.prototype.say = function() {
  console.log(this.name);
}
let cat1 = new Cat('tom'); 
let cat2 = new Cat('bob');
cat1.say === cat2.say 
cat1.say === Cat.prototype.say; // true
cat1.prototype; // undefined
cat1.hasOwnProperty('say');// false

__實例對象的constructor屬性指向其構造函數(1)__,這樣看起來實例對象好像「繼承」了prototype對象同樣。__實例沒有prototype__,上文最後一行代碼經過hasOwnPropertyk能夠判斷say這個方法並非cat1本身的方法,__若是一個方法沒有在實例對象自身找到,則向其構造函數prototype中開始尋找(2)__。

既然實例是繼承自構造器的prototype,那麼有沒有一個屬性能夠直接表示對象的繼承關係呢?答案是有的__proto__,不少瀏覽器都實現了這個屬性,以下所示。

cat1.__proto__ === Cat.prototype // true
Cat.__proto__ === Function.prototype; // true
Function.prototype.__proto__ === Object.prototype; // true

從上咱們能夠發現 Cat 構造器的原型爲Function.prototype ,Cat.prototype的原型爲Object.prototype,因此當cat1調toString時 Cat.prototype上沒有找到 就去Function.prototype上尋找,這就構成了原型鏈。可是對象的原型鏈查找和構造函數的原型查找又有一點小區別(不查Function),構造器生成的實例對象原型鏈的查找過程能夠以下表示:

cat1 
 => cat1.__proto__(Cat.prototype) 
 => cat1.__proto__.__proto__(Function.prototype) 
 => cat1.__proto__.__proto__.__proto__ (Object.prototype)

還有經過對象字面量建立的對象的原型鏈查找方式

let obj = {};
obj => obj.__proto__(Object.prototype) ;

這裏根據上文__加粗(2)__的語言能夠獲得__Function.prototype 的構造函數是Object(3)__。關於二者的關係,咱們後續繼續討論。

你們都有constructor

上文的兩個實例對象cat1 cat2,他們都具備一個屬性constructor,指向實例的構建函數Cat,意思是他們由Cat建立而來。__實例有一個constructor屬性,指向其構造函數(4)__

cat1.constructor === Cat; // true
cat1.constructor === Cat; // true
Cat.constructor === Function; // true
Cat.prototype.constructor === Cat; // true

Object.constructor === Function;// true

構造函數一樣具備construtor,指向Function,Cat.prototype一樣具備construtor,指向他自身,__構造函數的prototype對象的constructor指向該構造函數(5)__。

根據上文最後一行代碼 能夠判斷Object 的構造函數 是Function。則咱們能夠獲得Object是Function的一個實例。以下Object 與 Function的關係是

  1. Object是Function的一個實例。
  2. Function.prototype 是 Object 的 一個實例。

根據上文總結以下:

  1. 實例對象的constructor指向其構造器。
  2. 實例對象沒有prototype。
  3. 實例對象能夠經過構造函數的prototype對象實現屬性方法共享。’
  4. 實例對象的__proto__原型指向其構造函數的prototype對象
  5. 構造器的constructor指向 Function。
  6. 構造函數的prototype能夠掛在公共屬性方法,prototype的constructor屬性指向該構造函數。
  7. 構造函數的__proto__原型指向 Function.prototype。
  8. 構造函數prototype對象的__proto__原型指向Object.prototype。
  9. 對象原型指的是對象的__proto__屬性。

繼承方式的漸進式

經過上面的知識咱們已經瞭解了原型的概念,接下來咱們來一步一步實現基於原型的繼承。
在繼承以前,咱們有必要統一一下概念及名詞,

實例的歸實例 構造器的歸構造器
function Animal(name) {
  let name = name; // 私有屬性
  this.getName = function() { // 特權方法 也是實例方法
      this.log(name);
    return name;
  }
  this.color = 'none'; // 實例屬性
  this.say = function() { // 實例方法
    console.log(this.color);
  }
}
Animal.prototype.a = 1; // 公共屬性
Animal.prototype.log = function(sth) { // 公共方法
  consoel.log(sth)
}

js沒有嚴格意義的私有成員,因此對象屬性都算作公開,因此咱們在私有 公有上不作贅述,只是判斷改屬性是在實例上 仍是在構造函數的prototype上。

  1. 私有屬性:指的是構造器內部的屬性,構造器外部不能夠得到,只能經過特權方法來訪問。
  2. 特權方法:通常稱有權訪問私有變量和私有函數的公有方法爲特權方法,可是js沒有共有方法的概念,這個方法是掛載在實例上的。
  3. 實例屬性(方法):實例屬性指的是掛載在實例自身的屬性。
  4. 公共屬性(方法):公共屬性指的是掛在在構造器的prototype對象上的屬性。
1. 直接修改prototype

咱們已經知道實例對象能夠經過構造函數的prototype對象實現屬性方法共享。即實例對象繼承了構造器的.prototype對象,那麼構造器和構造器之間的繼承是否是也能夠用這樣的方式。

function Animal() {
  this.special = '貓';
};
function Cat() {}
let cat1 = new Cat();

如上,cat1要繼承Animal的special屬性,

  1. 首先 cat1 做爲構造器Cat 的一個實例能夠繼承 Cat.prototype 對象中得屬性。
  2. Cat.prototype 做爲一個對象則應該繼承 Animal.protoype.
  3. Cat.prototype 應該做爲構造函數Animal的一個實例。
function Animal() {
  this.special = '貓';
  this.arr = [2,3];
};
function Cat() {}
Cat.prototype = new Animal();
let cat1 = new Cat();
cat1.special; // '貓';

let cat2 = new Cat();
cat1.special = '狗';
cat2.special; // '貓'
cat1.special === Cat.prototype.special; // false
cat1.arr.push(1);
cat1.arr; // [2,3,1];
cat1.arr; // [2,3,1];

雖然咱們很簡單就實現了繼承,可是問題一轉變,就出現了bug。好比我如今但願cat1 cat2 的special 都是公共屬性,arr 是實例屬性。能夠發現cat1操做了special 這個公共屬性,cat2.special並無改變,可是cat1.arr 改變後 cat2.arr 也改變了。其次,構造器之間的繼承不能傳遞參數,那讓咱們更正2.0

2. 構造函數的函數特性
function Animal(name) {
  this.name = name;
  this.arr = [2,3];
};
Animal.prototype.special = '貓';

function Cat(name) { 
  Animal.apply(this, arguments);
}

Cat.prototype = new Animal();

let cat1 = new Cat('tom');
let cat2 = new Cat('mary');

cat1.special = '狗'; 
cat2.special; // 貓;
cat1.hasOwnProperty('special'); // true
cat2.hasOwnProperty('special;); // false,

cat1.arr.push(1);
cat1.arr; // [2,3,1];
cat2.arr; // [2,3];

cat1.name; // 'tom'
cat2.name; // 'mary'

special做爲公共的屬性掛載在父級構造器prototype上,雖然咱們修改了cat1.special cat2.special沒有改變,這主要是由於cat1.special 的改變是做用在實例而不是原型上,你們能夠把這個公共屬性改爲數組或對象 做爲一個引用存儲,就能夠發現special是公共屬性。cat1.arr的操做不影響cat2.arr的操做。並且能夠實現構造器直接傳參,這裏實在子級構造器的內部直接調用父級構造器,構造器調用方式的區別前文也介紹過了。

看到這裏,好像咱們已經實現繼承了,可是依然存在問題啊。代碼的構建歷來都是改大於寫。

cat1.constructor; // [Function: Animal]

前文提到實例對象的constructor屬性應該指向其構造函數,這裏直接指向了父級構造器;在Cat構造器內部有一份Animal的實例屬性,在Cat.prototype上一樣有一份Animal的實例屬性,屬性重複。

3. 利用空構造器過濾實例屬性
function Animal(name) {
  this.name = name;
  this.arr = [2,3];
};
Animal.prototype.special = '貓';

function Cat(name) { 
  Animal.apply(this, arguments);
}

let F = function() {};
F.prototype = Animal.prototype;
Cat.prototype = new F();
Cat.prototype.constructor = Cat;
Cat.__proto__ = Animal.prototype;

let cat1 = new Cat('tom');
let cat2 = new Cat('mary');

cat1.constructor;

這裏新建了一個空構造器 F() 讓F.prototype = Animal.prototype,子級構造器
Cat.prototype = new F(); 這樣在Cat.prototype中就沒有那一份Animal實例化以後的數據。再將Cat.prototype.constructor 從新指會 構造器自己,則cat1.constructor ye的指向也沒有問題了。同時修正了Cat的原型指向。

最後

首先感謝閱讀徹底文,到這裏,相信基本對於原型繼承實現面向對象編程沒有什麼問題了。以後的主要矛盾在於問題的抽象上,如何抽象合適的對象,哪些屬性和方法做爲公共的,哪些做爲實例的,這隻有日積月累的經驗才能給本身最好的答案。關鍵仍是在於理解了基礎概念,多用,多練,就會發先問題。我就是自覺得理解了,可是在construtor指向上老犯糊塗,還有關於Object 與 Function,多用是加深理解的最好方式了,不妨之後再解決問題是,多考慮一下面向對象。

其次,不能限定本身必須使用什麼,無論是黑貓仍是白貓,抓住老鼠就是好貓,代碼的最終目的是爲解決問題而生,同時代碼是用來讀的,不管是什麼樣的編程思路,邏輯清晰,可擴展,可複用,健壯性無缺那就是好代碼。

最後的最後,文中如有錯誤,還請及時指正。最後一個學習方法的分享,當接觸一個新的知識點或者工具,1.先會用 知道這個東西是什麼(what?) 怎麼用(how?), 2. 會用以後不妨瞭解一下原理看看內部實現(why?),3. 等研究的比較深入了,天然而然對在何種狀況使用(where, when)。編程學習仍是要帶着問題去學習,有問題,纔會記得更深入,沒問題的兩種人,要麼真的會了,要麼一點都不會,再次感謝閱讀~~~~

相關文章
相關標籤/搜索