摘要:面向對象中有三大特征,封裝,繼承,多態(tài)。這不僅無法做到數(shù)據(jù)共享,也是極大的資源浪費,那么引入對象實例對象的屬性指向其構造函數(shù),這樣看起來實例對象好像繼承了對象一樣。實例對象的原型指向其構造函數(shù)的對象構造器的指向。
前言
為什么說是再談呢,網(wǎng)上講解這個的博客的很多,我開始學習也是看過,敲過就沒了,自以為理解了就結束了,書到用時方恨少啊。實際開發(fā)中一用就打磕巴,于是在重新學習了之后分享出來。開了這么一個宏觀的題目,需要做一下簡單說明,這篇文章將會講解以下幾個問題:
什么是面向對象編程思想,為什么要用面向對象思想。
js中的面向對象思想和其他靜態(tài)語言相比有什么不同。
js中prototype,constructor,__proto__這些都是什么鬼?怎么用他們實現(xiàn)面向對象中的繼承。
一些小感悟小建議之類的吧啦吧啦。
下面我們直接開始干貨。。。
面向對象 what"s it? why ues it?什么是面向對象編程?當作者剛開始工作時,懷著對面向對象編程的無限敬仰和好奇,問了同事Java大牛這個問題,他的回答引我深思:不要面向對象編程,要面向工資編程。言歸正傳,面向對象中的對象,當然不是男女朋友的對象,ECMAScript中,對象是一個無序屬性集,這里的“屬性”可以是基本值、另一個對象或者函數(shù)。實際應用可以理解為一本書,一個人,一個班級,所以萬物都是對象。對象的屬性怎么理解,以人為例,指人的名字、身高、體重等等,對象的屬性還可以是函數(shù)稱之為方法,指代對象的一些操作,動作。如人的說話,走路等等。提到面向對象,那就需要提到面向過程,我們不用官方的方式來解釋,從實際問題中思考。
假設現(xiàn)在項目需求為畫一個三角形,一個矩形。直接編寫代碼時,我們肯定考慮的是第一步 畫三角形 第二步 畫矩形。我們會編寫一個三角形函數(shù)triangle() 一個矩形函數(shù)rect() 然后一步步調(diào)用,這是面向過程的思想。
function triangle() {...} function rect() {...} triangle(); rect();
面向對象中我們首先會抽象問題,矩形三角形都是對象,他們的類型都是形狀。他們有各自的邊長頂點,那么我們會先創(chuàng)建一個基本對象 形狀 Shape 屬性有頂點、邊長,三角形Triangle和矩形Rect都是基本對象擴展出的新對象,有各自的畫圖方法draw(),然后用對象得到具體的指向對象(即實例,后文解釋)triangle調(diào)用draw方法
function Shape() {...} function Triangle() {...} function Rect() {...} let triangle = new Triang(); triangle.draw(); let rect = new Rect(); rect.draw();
面對一個問題,面向過程的思路是第一步做什么,第二步做什么 面向對象則需要先分析出問題中的對象都有什么,對象的屬性、方法是什么,讓對象要做什么。
假設現(xiàn)在需要獲得畫出矩形的邊長,面向對象中只需要在Rect中加上一個方法就可以,面向過程則需要拿到畫出的矩形,再得到邊長,相比較而言面向對象易于擴展。
面向對象中有三大特征,封裝,繼承,多態(tài)。封裝指將變化封裝起來,外面調(diào)用時不需要知道內(nèi)部的實現(xiàn),繼承指的是一個對象可以共享父級對象的一些屬性,比如上文的問題中,形狀Shape有頂點這個屬性,三角形和矩形都可以繼承該屬性而不需要再重新定義。多態(tài)指的是封裝之后的變化如何處理,比如上文中將draw函數(shù)放在形狀Shape中,內(nèi)部實現(xiàn)就是連接點,三角形和矩形調(diào)用父級對象的draw,三角形與矩形的頂點不同。
為什么要使用面向對象?面向對象因為封裝,繼承,多態(tài)的特征使程序更易于擴展,維護,重用。比如在另外一個環(huán)境中我們需要畫三角形,我們只需要將三角形這個對象及形狀父級對象引入,剩下關于三角形的操作都是三角形這個對象的內(nèi)部實現(xiàn)。維護起來去該對象的該方法找錯,比在整個環(huán)境中找三角形函數(shù)要好很多。
js中的面向對象面向對象中類指的是同一類型對象的抽象,首字母大寫,比如上文中的形狀 Shape 類,三角形是通過Shape擴展而來,則也是一個類,Shape稱之為它的父類,它是Shape的子類,同理 Rect也是Shape的一個子類。類的具體抽象稱之為實例,通常為小寫,創(chuàng)建實例的過程稱之為實例化。上文中triangle就是一個Triangle三角形的實例,指具體畫出的那個三角形。關于父類,子類,實例我們再用一個一個示例來展示
父類 Animal 子類 Cat 實例 cat1_tom 子類 Dog 實例 dog1
Animal 指所有動物,Cat 指所有貓 繼承Animal 是動物的一個子類,cat1_tom 指的具體一個叫 tom 的貓。有了類我們就需要給類加一些標識,以區(qū)分類之間的區(qū)別、即屬性和方法。
1.走出‘類’,走進原型當我們弄清楚了類是什么,JavaScript沒有類的概念,是通過原型來實現(xiàn)面向對象。在以類為中心的面向對象編程語言中,類和對象的關系可以想象成鑄模和鑄件的關系,對象總是從類中創(chuàng)建而來。而在原型編程的思想中,類并不是必需的,對象未必需要從類中創(chuàng)建而來,一個對象是通過克隆另外一個對象所得到的。
從設計模式的角度講,原型模式是用于創(chuàng)建對象的一種模式,如果我們想要創(chuàng)建一個對象,一種方法是先指定它的類型,然后通過類來創(chuàng)建這個對象。原型模式選擇了另外一種方式,我們不再關心對象的具體類型,而是找到一個對象,然后通過克隆來創(chuàng)建一個一模一樣的對象。而克隆出來的這個對象會記住他的原型,由誰克隆而來,同時也會共享原型的屬性和方法。這樣一個一個對象克隆而來,則形成了一條原型鏈。對上文中的例子而言,三角形的原型是形狀,貓和狗的原型是動物。
2.構造函數(shù)在java中new Class()new 之后跟的是一個類名,而在js中類之后跟的是一個構造函數(shù)。
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");
構造函數(shù)的定義與一般函數(shù)的定義相同,注意首字母大寫。構造函數(shù)本質(zhì)上還是一個函數(shù),可以傳參可以有返回值,只是內(nèi)部使用了this變量,函數(shù)存在調(diào)用問題:
直接調(diào)用:在瀏覽器環(huán)境中相當于在window上掛在了val這個屬性,值為1。請注意這個特點,如果Shape.call(obj) 即相當于設定obj對象的val為1。
new 調(diào)用:生成一個實例,即生成一個新對象,這個this指向當前新生成的對象。
constructor和prototype這里的概念還希望大家閱讀緩慢 最好能在瀏覽器或者node環(huán)境下敲一下理解更深。請首先一定理解何為實例何為構造函數(shù)(構造器)。他們的關系是
__A為B的構造函數(shù) 則 B為A的一個實例__。
首先創(chuàng)建一個Cat的構造函數(shù),希望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
但是發(fā)現(xiàn)cat1 cat2的共有方法all并沒有共享,每一個實例對象,都有自己的屬性和方法的副本。這不僅無法做到數(shù)據(jù)共享,也是極大的資源浪費, 那么引入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屬性指向其構造函數(shù)(1)__,這樣看起來實例對象好像“繼承”了prototype對象一樣。__實例沒有prototype__,上文最后一行代碼通過hasOwnPropertyk可以判斷say這個方法并不是cat1自己的方法,__如果一個方法沒有在實例對象自身找到,則向其構造函數(shù)prototype中開始尋找(2)__。
既然實例是繼承自構造器的prototype,那么有沒有一個屬性可以直接表示對象的繼承關系呢?答案是有的__proto__,很多瀏覽器都實現(xiàn)了這個屬性,如下所示。
cat1.__proto__ === Cat.prototype // true Cat.__proto__ === Function.prototype; // true Function.prototype.__proto__ === Object.prototype; // true
從上我們可以發(fā)現(xiàn) Cat 構造器的原型為Function.prototype ,Cat.prototype的原型為Object.prototype,所以當cat1調(diào)toString時 Cat.prototype上沒有找到 就去Function.prototype上尋找,這就構成了原型鏈。但是對象的原型鏈查找和構造函數(shù)的原型查找又有一點小區(qū)別(不查Function),構造器生成的實例對象原型鏈的查找過程可以如下表示:
cat1 => cat1.__proto__(Cat.prototype) => cat1.__proto__.__proto__(Function.prototype) => cat1.__proto__.__proto__.__proto__ (Object.prototype)
還有通過對象字面量創(chuàng)建的對象的原型鏈查找方式
let obj = {}; obj => obj.__proto__(Object.prototype) ;
這里根據(jù)上文__加粗(2)__的語言可以得到__Function.prototype 的構造函數(shù)是Object(3)__。關于兩者的關系,我們后續(xù)繼續(xù)討論。
大家都有constructor上文的兩個實例對象cat1 cat2,他們都具有一個屬性constructor,指向實例的構建函數(shù)Cat,意思是他們由Cat創(chuàng)建而來。__實例有一個constructor屬性,指向其構造函數(shù)(4)__
cat1.constructor === Cat; // true cat1.constructor === Cat; // true Cat.constructor === Function; // true Cat.prototype.constructor === Cat; // true Object.constructor === Function;// true
構造函數(shù)同樣具有construtor,指向Function,Cat.prototype同樣具有construtor,指向他自身,__構造函數(shù)的prototype對象的constructor指向該構造函數(shù)(5)__。
根據(jù)上文最后一行代碼 可以判斷Object 的構造函數(shù) 是Function。則我們可以得到Object是Function的一個實例。如下Object 與 Function的關系是
Object是Function的一個實例。
Function.prototype 是 Object 的 一個實例。
根據(jù)上文總結如下:
實例對象的constructor指向其構造器。
實例對象沒有prototype。
實例對象可以通過構造函數(shù)的prototype對象實現(xiàn)屬性方法共享。’
實例對象的__proto__原型指向其構造函數(shù)的prototype對象
構造器的constructor指向 Function。
構造函數(shù)的prototype可以掛在公共屬性方法,prototype的constructor屬性指向該構造函數(shù)。
構造函數(shù)的__proto__原型指向 Function.prototype。
構造函數(shù)prototype對象的__proto__原型指向Object.prototype。
對象原型指的是對象的__proto__屬性。
繼承方式的漸進式通過上面的知識我們已經(jīng)了解了原型的概念,接下來我們來一步一步實現(xiàn)基于原型的繼承。
在繼承之前,我們有必要統(tǒng)一一下概念及名詞,
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沒有嚴格意義的私有成員,所以對象屬性都算做公開,所以我們在私有 公有上不做贅述,只是判斷改屬性是在實例上 還是在構造函數(shù)的prototype上。
私有屬性:指的是構造器內(nèi)部的屬性,構造器外部不可以獲得,只能通過特權方法來訪問。
特權方法:一般稱有權訪問私有變量和私有函數(shù)的公有方法為特權方法,但是js沒有共有方法的概念,這個方法是掛載在實例上的。
實例屬性(方法):實例屬性指的是掛載在實例自身的屬性。
公共屬性(方法):公共屬性指的是掛在在構造器的prototype對象上的屬性。
我們已經(jīng)知道實例對象可以通過構造函數(shù)的prototype對象實現(xiàn)屬性方法共享。即實例對象繼承了構造器的.prototype對象,那么構造器和構造器之間的繼承是不是也可以用這樣的方式。
function Animal() { this.special = "貓"; }; function Cat() {} let cat1 = new Cat();
如上,cat1要繼承Animal的special屬性,
首先 cat1 作為構造器Cat 的一個實例可以繼承 Cat.prototype 對象中得屬性。
Cat.prototype 作為一個對象則應該繼承 Animal.protoype.
Cat.prototype 應該作為構造函數(shù)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];
雖然我們很簡單就實現(xiàn)了繼承,但是問題一轉變,就出現(xiàn)了bug。比如我現(xiàn)在希望cat1 cat2 的special 都是公共屬性,arr 是實例屬性。可以發(fā)現(xiàn)cat1操作了special 這個公共屬性,cat2.special并沒有改變,但是cat1.arr 改變后 cat2.arr 也改變了。其次,構造器之間的繼承不能傳遞參數(shù),那讓我們更正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 的改變是作用在實例而不是原型上,大家可以把這個公共屬性改成數(shù)組或對象 作為一個引用存儲,就可以發(fā)現(xiàn)special是公共屬性。cat1.arr的操作不影響cat2.arr的操作。而且可以實現(xiàn)構造器直接傳參,這里實在子級構造器的內(nèi)部直接調(diào)用父級構造器,構造器調(diào)用方式的區(qū)別前文也介紹過了。
看到這里,好像我們已經(jīng)實現(xiàn)繼承了,但是依然存在問題啊。代碼的構建從來都是改大于寫。
cat1.constructor; // [Function: Animal]
前文提到實例對象的constructor屬性應該指向其構造函數(shù),這里直接指向了父級構造器;在Cat構造器內(nèi)部有一份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實例化之后的數(shù)據(jù)。再將Cat.prototype.constructor 重新指會 構造器本身,則cat1.constructor ye的指向也沒有問題了。同時修正了Cat的原型指向。
首先感謝閱讀完全文,到這里,相信基本對于原型繼承實現(xiàn)面向對象編程沒有什么問題了。之后的主要矛盾在于問題的抽象上,如何抽象合適的對象,哪些屬性和方法作為公共的,哪些作為實例的,這只有日積月累的經(jīng)驗才能給自己最好的答案。關鍵還是在于理解了基礎概念,多用,多練,就會發(fā)先問題。我就是自以為理解了,但是在construtor指向上老犯糊涂,還有關于Object 與 Function,多用是加深理解的最好方式了,不妨以后再解決問題是,多考慮一下面向對象。
其次,不能限定自己必須使用什么,不管是黑貓還是白貓,抓住老鼠就是好貓,代碼的最終目的是為解決問題而生,同時代碼是用來讀的,不論是什么樣的編程思路,邏輯清晰,可擴展,可復用,健壯性完好那就是好代碼。
最后的最后,文中若有錯誤,還請及時指正。最后一個學習方法的分享,當接觸一個新的知識點或者工具,1.先會用 知道這個東西是什么(what?) 怎么用(how?), 2. 會用之后不妨了解一下原理看看內(nèi)部實現(xiàn)(why?),3. 等研究的比較深刻了,自然而然對在何種情況使用(where, when)。編程學習還是要帶著問題去學習,有問題,才會記得更深刻,沒問題的兩種人,要么真的會了,要么一點都不會,再次感謝閱讀~~~~
文章版權歸作者所有,未經(jīng)允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/89389.html
摘要:設計模式是以面向對象編程為基礎的,的面向對象編程和傳統(tǒng)的的面向對象編程有些差別,這讓我一開始接觸的時候感到十分痛苦,但是這只能靠自己慢慢積累慢慢思考。想繼續(xù)了解設計模式必須要先搞懂面向對象編程,否則只會讓你自己更痛苦。 JavaScript 中的構造函數(shù) 學習總結。知識只有分享才有存在的意義。 是時候替換你的 for 循環(huán)大法了~ 《小分享》JavaScript中數(shù)組的那些迭代方法~ ...
摘要:有一函數(shù)若是用來生成對象,則稱為構造函數(shù)名。屬性指定了使用該構造函數(shù)生成的對象實例繼承了哪個對象實例。因此,只要利用,就能在構造函數(shù)中,為未來利用此構造函數(shù)生成的對象實例,添加成員屬性和成員方法了。 與其它編程語言不一樣的是,javascript的面向對象并非依賴于抽象的類,而是通過原型鏈,將一個個具體的對象實例進行連接,位于原型鏈下游的對象實例可以讀取/使用位于上游的對象實例的屬性/...
摘要:原型繼承基本模式這種是最簡單實現(xiàn)原型繼承的方法,直接把父類的對象賦值給子類構造函數(shù)的原型,這樣子類的對象就可以訪問到父類以及父類構造函數(shù)的中的屬性。 真正意義上來說Javascript并不是一門面向對象的語言,沒有提供傳統(tǒng)的繼承方式,但是它提供了一種原型繼承的方式,利用自身提供的原型屬性來實現(xiàn)繼承。Javascript原型繼承是一個被說爛掉了的話題,但是自己對于這個問題一直沒有徹底理解...
摘要:不必在構造函數(shù)中定義對象實例的信息。其次,按照一切事物皆對象的這餓極本的面向對象的法則來說,類本身并不是一個對象,然而原型方式的構造函數(shù)和原型本身也是個對象。第二個問題就是在創(chuàng)建子類型的實例時,不能向超類型的構造函數(shù)中傳遞參數(shù)。 前言 對象(Object)應該算是js中最為重要的部分,也是js中非常難懂晦澀的一部分。更是面試以及框架設計中各出沒。寫這篇文章,主要參考與JavaScrip...
摘要:在創(chuàng)建子類實例時,不能向超類型的構造函數(shù)中傳遞參數(shù)。構造函數(shù)繼承子類傳進的值是基本思想是在子類構造函數(shù)的內(nèi)部調(diào)用超類或父類型構造函數(shù)。繼承保證構造函數(shù)指針指向如果想同時繼承多個,還可使用添加屬性的方式類繼承, OOP:Object Oriented Programming 面向對象編程。 題外話:面向對象的范圍實在太大,先把這些大的東西理解理解。 1.什么是對象? 根據(jù)高程和權威指南上...
閱讀 3694·2021-11-11 10:58
閱讀 2476·2021-09-22 15:43
閱讀 2869·2019-08-30 15:44
閱讀 2188·2019-08-30 13:08
閱讀 1821·2019-08-29 17:28
閱讀 884·2019-08-29 10:54
閱讀 675·2019-08-26 11:46
閱讀 3507·2019-08-26 11:43