目錄
- 前言引入原型模式
- 頭腦風暴
- 傳統方式 vs 原型模式
- 實戰案例:飛機大戰中的分身術
- 原型模式實現的關鍵秘密
- 實戰演練:造一架能分身的飛機
- 克隆是創建對象的手段
- 原型模式:輕裝上陣的造物術
- 原型編程范型的一些規則
- 原型編程的四大門規:不會就問“我爹”
- 原型編程的四大鐵律(門規)
- 所有數據都是對象
- 想要新對象?別 new 類了,找個原型克隆一份!
- 對象會記得它的“親爹”是誰(原型)
- 如果對象不會干某件事,它會把任務交給它的“原型爸爸”
- JavaScript中的原型繼承
- 1?? 所有的數據都是對象(或接近)
- 2?? 要得到一個對象,不是通過實例化類,而是找到一個對象作為原型并克隆它
- 3?? 對象會記住它的原型
- 4?? 如果對象無法響應某個請求,它會把這個請求委托給它的構造器的原型
- 結語:
前言引入原型模式
本文的內容深受《JavaScript設計模式》
一書的啟發,特別是關于原型模式的討論,該書深入淺出地介紹了這一重要的設計模式及其在JavaScript
語言中的實現。原型模式不僅是眾多設計模式中的一員,它更是構建JavaScript
這門語言基礎的核心之一。通過這本書,我們得以從更加簡單的Io
語言入手,逐步理解原型模式的概念,并學習如何在JavaScript
中應用這一模式來創建強大而靈活的對象系統。
加之在面試過程中,對原型,原型鏈,圓形模式都很模糊,對《JavaScript設計模式》書中的原型模式進行深入反復學習將定義及理論理變為自己的語言重新組織輸出。
頭腦風暴
想象一下,你是一個程序員界的“Ctrl+C / Ctrl+V”
大師。你不想每次都重新寫代碼,也不想手動配置一堆參數。你只想——一鍵克隆,天下我有!這,就是我們今天要說的主角:原型模式(Prototype Pattern
)。
它不只是設計模式,它是一種編程哲學!
原型模式不僅僅是一個設計模式,它還是一個“編程泛型”級別的存在。你可以把它理解為:
“別跟我說什么類、繼承、
new
對象那一套,給我一個樣板,我能克隆出一整個世界。”
從設計模式的角度來看,原型模式的核心思想是:與其造個類慢慢 new
,不如找個現成的,咔嚓一下,直接復制一份!
傳統方式 vs 原型模式
通常我們要創建一個對象,得先定義一個類,然后 new
出來一個實例。比如:
let plane = new FighterPlane("紅色", 100, "高級炮彈", 50);
這看起來很標準,但問題是:如果你想造一個跟這個飛機一模一樣的分身呢?血量、武器、防御值、皮膚顏色……都得一個個手動傳進去?那不是要命嗎?你是寫代碼的,又不是在填表格!這時候,原型模式就閃亮登場了!原型模式是怎么干的?
原型模式說:
“嘿,別整這些麻煩事了。你不是已經有一個完美的飛機了嗎?拿它當模板,克隆一個不就完了?”
于是你就調用了一個方法,比如:
let clonePlane = originalPlane.clone();
一句話搞定,啥都不用管。原飛機有什么屬性,新飛機就自動擁有,連它的“壞脾氣”都一起復制過去了!
實戰案例:飛機大戰中的分身術
假設你在開發一款網頁游戲《飛機大戰》,某個 boss
飛機突然大喊一聲:“我要分身!”,你是不是得手忙腳亂地記下它當前的血量、攻擊力、裝備等級、飛行姿勢……然后再 new
出一個一樣的?不用了!現在只要一句:
let 分身 = 真身.clone();
克隆出來的分身不僅長得像,連戰斗狀態都同步了。真·完美復制!如果使用原型模式,我們只需要調用負責克隆的方法,便能完成同樣的功能。
原型模式實現的關鍵秘密
通過上面講解你還以為程序員只會 new
對象?不不不,我們還有更高級的操作——復制粘貼對象!
而實現這個魔法的關鍵,就是看語言有沒有提供一個叫 .clone()
的方法。可惜的是,JavaScript
并沒有原生的 .clone() 方法
(別哭別沮喪,JS
本來就不是那種貼心暖男型又或者貼心鄰家大姐姐語言),但它給了我們另一個工具箱里的神器:Object.create()
,這玩意兒就像是 JavaScript
界的“克隆羊多利”,只要你給它一個“母體對象”,它就能給你造出一個一模一樣的副本!
實戰演練:造一架能分身的飛機
讓我們來看一段代碼,看看怎么用 Object.create() 把飛機克隆出來!
var Plane = function() { this.blood = 100; this.attackLevel = 1; this.defenseLevel = 1; }; var plane = new Plane(); plane.blood = 500; plane.attackLevel = 10; plane.defenseLevel = 7;
這段代碼干了啥?你可以理解為:我們先 new 出了一個基礎版小破飛機,然后給它加了個 buff,血量 +400,攻擊力 +9,防御力也猛漲。現在它已經是一架“戰神級飛機”了!那問題來了:你想再搞一架一模一樣的飛機怎么辦?難道要重新 new,再一個個屬性設置一遍?太麻煩了!這時候,就該請出我們的主角登場了:💥 克隆大法好!
var clonePlane = Object.create(plane);
一句話,搞定克隆!就像你拿著這架飛機去復印店說:“老板,來一份復印件,不要改樣式,我要原樣再來一份。”于是你就得到了一架新的飛機,連它的“戰斗狀態”都一毛一樣:
console.log(clonePlane); // 輸出:Object {blood: 500, attackLevel: 10, defenseLevel: 7}
但是捏,老舊瀏覽器怎么辦?有困難就解決困難!發動手動縫合克隆術!但總有那么幾個“古董級瀏覽器”,比如IE8
及以下,它們一臉懵逼地說:“什么?Object.create
是個啥?”,那怎么辦?別慌,我們可以自己寫一個“土法”克隆術:
Object.create = Object.create || function(obj) {var F = function() {};F.prototype = obj;return new F();
}
這相當于你在對瀏覽器說:
“既然你不支持克隆技術,那我就自己搭個克隆實驗室!用 prototype 搞點遺傳工程,照樣能造出一模一樣的飛機!”
雖然看起來有點土,但效果一樣頂呱呱!
克隆是創建對象的手段
通過上面飛機分身復制術,你以為原型模式只是個“復制粘貼工具人”?錯!它真正的身份是——宇宙造物主級別的對象工廠!別看它表面是在“克隆”,其實它心里想的是:
“我不只是在復制一個對象,我是在幫你創建一個新的世界。”
🤔 克隆只是手段,造對象才是目的!就像你去餐廳點了一份紅燒肉,廚師說:“哎呀今天沒肉了,我就給你端了一盤一模一樣的昨天剩菜。”雖然看起來一樣,但本質不一樣!
原型模式也是一樣:
表面上看:它在“復制”一個對象。
實際上:它是在用“復制”這種方式,來創建一個新對象。
換句話說:克隆只是過程,不是目的。就像洗澡是為了干凈,不是為了泡水。
💼 舉個 Java
程序員的苦逼例子
在 Java
這種“類型潔癖癥晚期”語言中,寫代碼就像是在做數學證明題:類型必須嚴格匹配,創建對象要 new
某個具體的類名,如果你想換實現?不好意思,得改代碼、加依賴、重新編譯……,這時候設計模式就站出來說話了:
“兄弟,咱們得解耦啊!”
“不能直接 new 對象,得搞個工廠出來!”
于是就有了:
工廠方法模式(Factory Method)
抽象工廠模式(Abstract Factory)
結果呢?本來只是想造一架飛機,現在還得先建個“飛機制造工廠公司集團有限公司”。更慘的是,每個飛機型號都得配一個對應的工廠……這代碼量,簡直爆炸!
原型模式:輕裝上陣的造物術
這時候原型模式閃亮登場了,它甩掉所有繁瑣的類和工廠,只說一句話:
“別整那些虛的,給我一個樣板,我能造出一個一模一樣的。”
這就像一個小女孩指著商店里的玩具飛機說:
“我要這個!”
而不是說:
“我要一個飛行器,材質塑料,動力系統為螺旋槳驅動,翼展
15cm
……”
她不懂那么多術語,但她知道:這個就是我要的對象!所以,原型模式的本質是:
? 不需要知道具體類名
? 不需要 new 出來一堆耦合
? 只要有一個對象,就能作為模板,輕松造出新的對象!
當然啦,在 JavaScript
的世界里,這一切變得更加絲滑。因為 JS
本身就是基于原型的語言,它不靠“類”來創建對象,而是靠“原型鏈”來繼承屬性。你可以理解為:JS
的對象系統,就是用原型模式搭起來的。所以從這個角度講,在 JavaScript 中使用原型模式,有點像是:
“給貓裝胡須,給魚裝鰓,給程序員發咖啡。”
已經自帶技能了好嗎!不過,如果你真要用原型模式來做業務邏輯上的對象創建,也不是不行。比如:
let plane = { blood: 500,attackLevel: 10,defenseLevel: 7,fire: function() {console.log("發射導彈!");}
};
let clonePlane = Object.create(plane);
clonePlane.name = "分身一號";
你看,不需要 new Plane()
,也不需要寫構造函數,只要有個原型對象,就可以直接克隆出一個新對象。是不是很像魔法?是不是比寫一堆 class
和factory
干凈多了?
📚 總結一下:原型模式到底圖啥?
傳統模式 | 原型模式 |
---|---|
new XXX(),依賴具體類 | 克隆已有對象,不關心類名 |
需要工廠類支持 | 一行代碼搞定 |
容易耦合 | 更加靈活 |
原型編程范型的一些規則
原型編程的四大門規:不會就問“我爹”
在 JavaScript
的江湖里,有個神秘的門派叫——原型宗(Prototype Sect
)。這個門派不講“類”
,不搞“繼承”
,他們只信奉一個真理:
“不會?沒問題,去問你爹。”
這,就是我們今天要說的——原型編程范型的基本規則。
原型編程的四大鐵律(門規)
所有數據都是對象
沒錯,在這里沒有“原始類型”這種說法,哪怕是數字、字符串,都被當作“對象小弟”來看待。你可以理解為:在原型宗的地盤上,連數字都想當大哥。
let x = 5;
x.isAlsoAnObject = true; // 雖然 JS 會臨時包裝成對象,但意思到了就行 😄
想要新對象?別 new 類了,找個原型克隆一份!
別人創建對象靠 new Plane()
,原型宗靠“復印機”。只要找到一個現成的對象作為“模板”,輕輕一按:
“Ctrl+C
,Ctrl+V
,一個一模一樣的飛機就出來了。”
let plane = { blood: 500, attackLevel: 10 };
let clonePlane = Object.create(plane);
一句話搞定,啥都不用寫!
對象會記得它的“親爹”是誰(原型)
每個對象心里都清楚,自己是從哪個原型克隆來的。就像孩子知道自己老爸是誰一樣,JS
中的對象也有一條“血緣鏈”——原型鏈(Prototype Chain
)你雖然沒有顯式地聲明繼承關系,但它默默地記住了它的“原型爸爸”。
如果對象不會干某件事,它會把任務交給它的“原型爸爸”
這是原型宗最核心的一句話:
“我不行?沒關系,我爹行!”
比如你想讓一個對象執行某個方法,它自己沒這個技能,它就會順著原型鏈往上找:
自己有沒有?沒有。
爸爸有沒有?有!借來用!
這就叫做:委托機制(Delegation
)
let dog = {bark: function() {console.log("汪汪汪!");}
};
let buddy = Object.create(dog);
buddy.bark(); // 輸出:"汪汪汪!",雖然 buddy 自己沒定義這個方法
🧾 總結一下:原型宗四大門規
規則編號 | 內容描述 | 解釋 |
---|---|---|
Rule 1 | 所有數據都是對象 | 數字也會裝逼說自己是對象 |
Rule 2 | 得到對象靠克隆原型,不是靠 new 類 不寫構造函數 | 只管復制粘貼 |
Rule 3 | 對象知道自己的原型是誰 | 孩子知道自己老爸是誰 |
Rule 4 | 請求失敗時,委托給原型處理 自己不會? | 去找你爹! |
JavaScript中的原型繼承
在 JavaScript
的世界里,沒有 Java
那種“類”的概念,它走的是另一條路——原型編程(Prototype-based Programming
)。這就像:別人靠“父母”生孩子,JS
靠“克隆”造對象。現在我們就來看看 JavaScript
是如何通過原型鏈實現對象之間的繼承關系的。
1?? 所有的數據都是對象(或接近)
JavaScript
在設計之初模仿了 Java
,引入了兩套類型機制:基本類型
和對象類型
。基本類型包括 undefined
、number
、boolean
、string
、function
和 object
。但說實話,這種設計有點迷糊。按照 JS
設計者的初衷,除了 undefined
外,其他一切都是對象。為了讓 number
、boolean
、string
這些基本類型也能像對象一樣被處理,JS
引入了“包裝類”。雖然不能說所有數據都是對象,但可以說絕大部分數據是對象。而且,在 JavaScript
中有一個根對象存在,那就是 Object.prototype
。它是所有對象的老祖宗,所有對象追根溯源都來源于這個根對象。
比如下面的例子:
var obj1 = new Object();
var obj/XMLSchema = {};
console.log(Object.getPrototypeOf(obj1) === Object.prototype); // 輸出:true
console.log(Object.getPrototypeOf(obj2) === Object.prototype); // 輸出:true
這就像是所有的對象都認同一個老祖宗——Object.prototype
。
2?? 要得到一個對象,不是通過實例化類,而是找到一個對象作為原型并克隆它
在 JavaScript
中,我們并不需要關心克隆一個對象的這些細節,因為這是引擎內部的事兒。當我們調用 new Object()
或者 {}
來創建對象時,引擎會從 Object.prototype
上面克隆出一個新的對象。再來看個例子:
function Person(name) { this.name = name; }Person.prototype.getName = function() { return this.name; };var a = new Person('sven');console.log(a.name); // 輸出:sven console.log(a.getName()); // 輸出:sven console.log(Object.getPrototypeOf(a) === Person.prototype); // 輸出:true
x
雖然我們用了 new
關鍵字,但其實并沒有真正意義上的“類”,Person
只是個構造器。使用 new
創建對象的過程,實際上也只是先克隆 Object.prototype
對象,再進行一些額外操作。
3?? 對象會記住它的原型
每個對象都記得自己的“親爹”是誰(即它的原型)。為了實現這一點,JavaScript
給對象提供了一個隱藏屬性——__proto__
。這個屬性指向對象的構造器的原型對象 {Constructor}.prototype
。
例如:
var a = new Object();
console.log(a.__proto__ === Object.prototype); // 輸出:true
這就像是對象之間有一條看不見的線,把它們串在一起。當我們用new
創建對象時,需要手動設置 obj.__proto__ = Constructor.prototype
,這樣才能讓對象正確地找到它的“親爹”。
4?? 如果對象無法響應某個請求,它會把這個請求委托給它的構造器的原型
這條規則就是原型繼承的精髓所在。當一個對象無法響應某個請求時,它會順著原型鏈把請求傳遞下去,直到遇到一個能處理該請求的對象為止。
舉個例子:
var obj = { name: 'sven' };
var A = function(){};
A.prototype = obj;
var a = new A();
console.log(a.name); // 輸出:sven
在這個過程中,如果對象 a
沒有找到 name
屬性,它就會沿著原型鏈向上查找,直到在 obj
中找到了name
屬性,并返回其值。再看一個稍微復雜一點的例子:
var A = function(){};
A.prototype = { name: 'sven' };
var B = function(){};
B.prototype = new A();
var b = new B();
console.log(b.name); // 輸出:sven
當嘗試訪問 b
的 name
屬性時,如果 b
自己沒有這個屬性,它會沿著原型鏈向上查找,直到在 A.prototype
中找到 name
屬性,并返回其值。
再看這段代碼執行的時候,引擎做了什么事情:
- 首先,嘗試遍歷對象
b
中的所有屬性,但沒有找到name
這個屬性。- 查找 name 屬性的請求被委托給對象 b 的構造器的原型,它被
b.__proto__
記錄著并且指向
B.prototype,而 B.prototype 被設置為一個通過 new A()創建出來的對象。- 在該對象中依然沒有找到 name 屬性,于是請求被繼續委托給這個對象構造器的原型A.prototype。
- 在 A.prototype 中找到了 name 屬性,并返回它的值。
????????和把 B.prototype
直接指向一個字面量對象相比,通過 B.prototype = new A()
形成的原型鏈比之前多了一層。但二者之間沒有本質上的區別,都是將對象構造器的原型指向另外一個對象,繼承總是發生在對象和對象之間。
????????最后還要留意一點,原型鏈并不是無限長的。現在我們嘗試訪問對象 a
的 address
屬性。而對象 b
和它構造器的原型上都沒有 address
屬性,那么這個請求會被最終傳遞到哪里呢?
實際上,當請求達到 A.prototype
,并且在 A.prototype
中也沒有找到 address
屬性的時候,請求會被傳遞給 A.prototype
的構造器原型 Object.prototype
,顯然 Object.prototype
中也沒有address
屬性,但 Object.prototype
的原型是 null
,說明這時候原型鏈的后面已經沒有別的節點了。所以該次請求就到此打住,a.address
返回 undefined
。
a.address // 輸出:undefined
🧩 總結一下:原型繼承的核心思想
規則編號 | 內容描述 | 理解 |
---|---|---|
Rule 1 | 所有數據都是對象(或接近) | 老祖宗是 Object.prototype |
Rule 2 | 得到對象靠克隆原型,不是靠 new 類 | new 類 不寫構造函數,只管復制粘貼 |
Rule 3 | 對象知道自己的原型是誰 | 孩子知道自己老爸是誰 |
Rule 4 | 請求失敗時,委托給原型處理 | 自己不會?去找你爹 |
💬 最后一句靈魂總結:
在 JavaScript 的世界里,你不靠“類”吃飯,你靠“原型”混江湖。遇事不懂?先問問你爹再說!
結語:
本文的內容深受《JavaScript設計模式》一書的啟發,特別是關于原型模式的討論,該書深入淺出地介紹了這一重要的設計模式及其在JavaScript語言中的實現。原型模式不僅是眾多設計模式中的一員,它更是構建JavaScript這門語言基礎的核心之一。通過這本書,我們得以從更加簡單的Io語言入手,逐步理解原型模式的概念,并學習如何在JavaScript中應用這一模式來創建強大而靈活的對象系統。
在此,我對《JavaScript設計模式》的作者表示深深的感謝和敬意。是你細致入微的講解讓復雜的設計模式變得易于理解,也為像我這樣的開發者提供了寶貴的指導和靈感。如果你希望深入了解JavaScript中的設計模式及其背后的原理,《JavaScript設計模式》絕對是一本不容錯過的好書!
致敬—— 《JavaScript設計模式》· 曾探