爲何說是再談呢,網上講解這個的博客的不少,我開始學習也是看過,敲過就沒了,自覺得理解了就結束了,書到用時方恨少啊。實際開發中一用就打磕巴,因而在從新學習了以後分享出來。開了這麼一個宏觀的題目,須要作一下簡單說明,這篇文章將會講解如下幾個問題:java
下面咱們直接開始乾貨。。。node
什麼是面向對象編程?看成者剛開始工做時,懷着對面向對象編程的無限敬仰和好奇,問了同事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
爲何要使用面向對象?面向對象由於封裝,繼承,多態的特徵使程序更易於擴展,維護,重用。好比在另一個環境中咱們須要畫三角形,咱們只須要將三角形這個對象及形狀父級對象引入,剩下關於三角形的操做都是三角形這個對象的內部實現。維護起來去該對象的該方法找錯,比在整個環境中找三角形函數要好不少。編程語言
面向對象中類指的是同一類型對象的抽象,首字母大寫,好比上文中的形狀 Shape
類,三角形是經過Shape
擴展而來,則也是一個類,Shape
稱之爲它的父類,它是Shape
的子類,同理 Rect
也是Shape
的一個子類。類的具體抽象稱之爲實例,一般爲小寫,建立實例的過程稱之爲實例化。上文中triangle
就是一個Triangle
三角形的實例,指具體畫出的那個三角形。關於父類,子類,實例咱們再用一個一個示例來展現函數
父類 Animal 子類 Cat 實例 cat1_tom 子類 Dog 實例 dog1
Animal 指全部動物,Cat 指全部貓 繼承Animal 是動物的一個子類,cat1_tom 指的具體一個叫 tom 的貓。有了類咱們就須要給類加一些標識,以區分類之間的區別、即屬性和方法。工具
當咱們弄清楚了類是什麼,JavaScript沒有類的概念,是經過原型來實現面向對象。在以類爲中心的面向對象編程語言中,類和對象的關係能夠想象成鑄模和鑄件的關係,對象老是從類中建立而來。而在原型編程的思想中,類並非必需的,對象未必須要從類中建立而來,一個對象是經過克隆另一個對象所獲得的。
從設計模式的角度講,原型模式是用於建立對象的一種模式,若是咱們想要建立一個對象,一種方法是先指定它的類型,而後經過類來建立這個對象。原型模式選擇了另一種方式,咱們再也不關心對象的具體類型,而是找到一個對象,而後經過克隆來建立一個如出一轍的對象。而克隆出來的這個對象會記住他的原型,由誰克隆而來,同時也會共享原型的屬性和方法。這樣一個一個對象克隆而來,則造成了一條原型鏈。對上文中的例子而言,三角形的原型是形狀,貓和狗的原型是動物。
在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變量,函數存在調用問題:
這裏的概念還但願你們閱讀緩慢 最好能在瀏覽器或者node環境下敲一下理解更深。請首先必定理解何爲實例何爲構造函數(構造器)。他們的關係是
__A爲B的構造函數 則 B爲A的一個實例__。
首先建立一個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)__。關於二者的關係,咱們後續繼續討論。
上文的兩個實例對象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的關係是
根據上文總結以下:
__proto__
原型指向其構造函數的prototype對象__proto__
原型指向 Function.prototype。__proto__
原型指向Object.prototype。__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上。
咱們已經知道實例對象能夠經過構造函數的prototype對象實現屬性方法共享。即實例對象繼承了構造器的.prototype對象,那麼構造器和構造器之間的繼承是否是也能夠用這樣的方式。
function Animal() { this.special = '貓'; }; function Cat() {} let cat1 = new Cat();
如上,cat1要繼承Animal的special屬性,
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
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的實例屬性,屬性重複。
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)。編程學習仍是要帶着問題去學習,有問題,纔會記得更深入,沒問題的兩種人,要麼真的會了,要麼一點都不會,再次感謝閱讀~~~~