類和原型的設計模式之復制與委托差異
小引
JavaScript 技能持有者一定有問過這個問題:
JavaScript 是面向?qū)ο笳Z言嗎?
你期望得到的答案應該為:“是” 或 “不是”。
但是可惜,你得不到這樣簡單的答案!
你大概了解一通之后,你會被告知:
JavaScript 不是純粹的面向?qū)ο笳Z言!
wtf!為什么是不純粹?能不能純粹一點?!我們喜歡純粹,不喜歡混沌!
......
實際上,死扣定義真的沒太必要。定義背后的故事才是最重要的!
看完本篇,你就會明白這種“混沌”是什么、來自何處,以及去往何方?。?/p>
撰文不易,多多鼓勵。點贊再看,養(yǎng)成習慣。??????
“類”設計模式
婦孺皆知,面向?qū)ο笕筇匦裕骸痉庋b】、【繼承】、【多態(tài)】。
- 所謂封裝,即把客觀事物封裝成抽象的類。
- 所謂繼承,即子類繼承父類的能力。
- 所謂多態(tài),即子類可以用更特殊的行為重寫所繼承父類的通用行為。
其中,“類”的概念最最關鍵!【類】描述了一種代碼的組織結(jié)構形式,它是軟件中對真實世界中問題領域的建模方法。
舉個例子:
就好比我們現(xiàn)實中修房子,你要修一個“寫字樓”、或者一個“居民樓”、或者一個“商場”,你就得分別找到修“寫字樓”、“居民樓”、“商場”的【設計藍圖】。
但是設計藍圖只是一個建筑計劃,并不是真正的建筑。要想在真實世界實現(xiàn)這個建筑,就得由建筑工人將設計藍圖的各類特性(比如長寬高、功能)【復制】到現(xiàn)實世界來。
這里的【設計藍圖】就是【類】,【復制】的過程就是【實例化】,【實例】就是【對象】。
類的內(nèi)部通常有一個同名的構造方法,我們設想下,它的偽代碼就可能是這樣的:
class Mall { // “商場”類 Mall( num ){ // 同名構造方法 garage = num // 地下車庫數(shù)量 } shop( goods ) { // 買東西 output( "We can buy: ", goods ) } } // 構造函數(shù)大多需要用 new 來調(diào),這樣語言引擎才知道你想要構造一個新的類實例。 vanke = new Mall(1) // vanke 有 1 個地下車庫 vanke.shop("KFC") // "We can buy: KFC"
java 是典型的面向?qū)ο笳Z言?;?ldquo;類”,我們再通過以下一段 java 代碼來看看對繼承和多態(tài)的理解。
public abstract class Animal{ // 抽象類 abstract void sound(); } public class Chicken extends Animal{ // 繼承 public void sound(){ sound("咯咯咯"); } } public class Duck extends Animal{ public void sound(){ sound("嘎嘎嘎"); } } public static void main(String args[]){ Aninal chicken = new Chicken(); Animal duck = new Duck(); chicken.sound(); //咯咯咯 duck.sound(); //嘎嘎嘎 }
雞和鴨都屬于動物分類,都可以發(fā)出叫聲(繼承),但是它們卻可以發(fā)出不同的叫聲(多態(tài)),很容易理解。
繼承可以使子類獲得父類的全部功能; 多態(tài)可以使程序有良好的擴展;
回想下:在 JS 中,我們可能會怎樣寫:
var Duck = function () {}; var Chicken = function () {}; var makeSound = function ( animal ) { if( animal instanceof Duck){ console.log("嘎嘎嘎"); }else if( animal instanceof Chicken){ console.log("咯咯咯"); } }; makeSound(new Duck()); makeSound(new Chicken());
這里既沒用到繼承,也沒用到多態(tài)。這樣【寫判斷】是代碼“不清爽”的罪魁禍首!
此處留一個疑問,如果不用判斷,還可以怎么寫?
在 vue2 中,我們可能會這么寫:
export default { data() { return { }, mounted(){ this.Chicken() this.Duck() }, methods:{ funtion AnimalSound(sound){ console.log("叫聲:" + sound) }, funtion Chicken(){ this.AnimalSound("咯咯咯") }, funtion Duck(){ this.AnimalSound("嘎嘎嘎") } } }
像這種函數(shù)嵌套調(diào)用是很常見的。沒有看到繼承,也沒有看到多態(tài),甚至都沒有看到最根本的“類”?!
(實際上,每個函數(shù)都是一個 Function 對象。按照最開始定義所述,對象是類的實例,所以也是能在函數(shù)中看到“類”的?。?/p>
在 JavaScript 中,函數(shù)成了第一等公民! 函數(shù)似乎什么都能做!它可以返回一個對象,可以賦值給一個變量,可以作為數(shù)組項,可以作為對象的一個屬性......
但這明顯不是“類的設計模式”吧!
“類的設計模式” 意味著對【設計藍圖】的【復制】,在 JS 各種函數(shù)調(diào)用的場景下基本看不到它的痕跡。
“原型”設計模式
其實,眾所周知,JS 也是能做到【繼承】和【多態(tài)】的!只不過它不是通過類復制的方式,而是通過原型鏈委托的方式!
一圖看懂原型鏈?
看不懂?沒關系,記住這兩句話再來看:
- 一個對象的顯示原型的構造函數(shù)指向?qū)ο蟊旧恚ê苁煜び袥]有?在本文哪里見過?)
- 一個對象的隱式原型指向構造這個對象的函數(shù)的顯示原型。
原來,JS 不是通過在類里面寫同名構造函數(shù)的方式來進一步實現(xiàn)的實例化,它的構造函數(shù)在原型上!這種更加奇特的代碼服用機制有異于經(jīng)典類的代碼復用體系。
這里再附一個經(jīng)典問題?JS new 操作會發(fā)生什么?
會是像類那樣進行復制嗎?
答案是否定的!
JS 訪問一個對象的屬性或方法的時候,先在對象本身中查找,如果找不到,則到原型中查找,如果還是找不到,則進一步在原型的原型中查找,一直到原型鏈的最末端。復制不是它所做的,這種查找的方式才是!對象之間的關系更像是一種委托關系,就像找東西,你在我這找不到?就到有委托關系的其它人那里找找看,再找不到,就到委托委托關系的人那里找......直至盡頭,最后還找不到,指向 null。
所以:JavaScript 和面向?qū)ο蟮恼Z言不同,它并沒有類來作為對象的抽象模式或者設計藍圖。JavaScript 中只有對象,對象直接定義自己的行為。對象之間的關系是委托關系,這是一種極其強大的設計模式。在你的腦海中對象并不是按照父類到子類的關系垂直組織的,而是通過任意方向的委托關聯(lián)并排組織的!
不過你也可以通過這種委托的關系來模擬經(jīng)典的面向?qū)ο篌w系:類、繼承、多態(tài)。但“類”設計模式只是一種可選的設計模式,你可以模擬,也可以不模擬!
現(xiàn)實是 ES6 class 給我們模擬了:
class Widget { constructor(width,height) { this.width = width || 50; this.height = height || 50; this.$elem = null; } render($where){ if (this.$elem) { this.$elem.css( { width: this.width + "px", height: this.height + "px" }).appendTo( $where ); } } } class Button extends Widget { constructor(width,height,label) { super( width, height ); this.label = label || "Default"; this.$elem = $( "<button>" ).text( this.label ); } render($where) { super.render( $where ); this.$elem.click( this.onClick.bind( this ) ); } onClick(evt) { console.log( "Button '" + this.label + "' clicked!" ); } }
看起來,非常不錯,很清晰!
沒有 .prototype 顯示原型復雜的寫法,也無需設置 .proto 隱式原型。還似乎用 extends 、super 實現(xiàn)了繼承和多態(tài)。
然而,這只是語法糖的陷阱!JS 沒有類,沒有復制,它的機制是“委托”。
class 并不會像傳統(tǒng)面向類的語言一樣在申明時作靜態(tài)復制的行為,如果你有意或者無意修改了父類,那子類也會收到影響。
舉例:
class C { constructor() { this.num = Math.random(); } rand() { console.log( "Random: " + this.num ); } } var c1 = new C(); c1.rand(); // "Random: 0.4324299..." C.prototype.rand = function() { console.log( "Random: " + Math.round( this.num * 1000 )); }; var c2 = new C(); c2.rand(); // "Random: 867" c1.rand(); // "Random: 432" ——噢!
ES6 class 混淆了“類設計模式”和“原型設計模式”。它最大的問題在于,它的語 法有時會讓你認為,定義了一個 class 后,它就變成了一個(未來會被實例化的)東西的 靜態(tài)定義。你會徹底忽略 Class 是一個對象,是一個具體的可以直接交互的東西。當然,它還有其它細節(jié)問題,比如屬性覆蓋方法、super 綁定的問題,有興趣自行了解。
總地來說,ES6 的 class 想偽裝成一種很好的語法問題的解決方案,但是實際上卻讓問題更難解決而且讓 JavaScript 更加難以理解。 —— 《你不知道的 JavaScript》
小結(jié)
- “類設計模式”的構造函數(shù)掛在同名的類里面,類的繼承意味著復制,多態(tài)意味著復制 + 自定義。
- “原型設計模式”的構造函數(shù)掛在原型上,原型的查找是一種自下而上的委托關系。
- “類設計模式”的類定義之后就不支持修改。
- “原型設計模式”講究的是一種動態(tài)性,任何對象的定義都可以修改,這和 JavaScript 作為腳本語言所需的動態(tài)十分契合!
你可以用“原型設計模式”來模擬“類設計模式”,但是這大概率是得不償失的。
最后,如果再被問道:JavaScript 是面向?qū)ο笳Z言嗎?
如果這篇文章看懂了,就可以圍繞:“類設計模式”和“原型設計模式”來吹了。
如果本文沒有看懂,就把下面的標答背下來吧......
參考
以上就是類和原型的設計模式之復制與委托差異的詳細內(nèi)容,更多關于類原型設計模式復制委托差異的資料請關注腳本之家其它相關文章!
相關文章
dotenv源碼解讀從.env文件中讀取環(huán)境變量
這篇文章主要為大家介紹了dotenv源碼解讀從.env文件中讀取環(huán)境變量示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-12-12