JavaScript 技能持有者必定有問過這個問題:css
JavaScript 是面嚮對象語言嗎?html
你指望獲得的答案應該爲:「是」 或 「不是」。vue
可是惋惜,你得不到這樣簡單的答案!java
你大概瞭解一通以後,你會被告知:git
JavaScript 不是純粹的面嚮對象語言!express
wtf!爲何是不純粹?能不能純粹一點?!咱們喜歡純粹,不喜歡混沌!編程
......設計模式
實際上,死扣定義真的沒太必要。定義背後的故事纔是最重要的!數組
看完本篇,你就會明白這種「混沌」是什麼、來自何處,以及去往何方!!markdown
撰文不易,多多鼓勵。點贊再看,養成習慣。👍👍👍
婦孺皆知,面向對象三大特性:【封裝】、【繼承】、【多態】。
所謂封裝,即把客觀事物封裝成抽象的類。
所謂繼承,即子類繼承父類的能力。
所謂多態,即子類能夠用更特殊的行爲重寫所繼承父類的通用行爲。
其中,「類」的概念最最關鍵!【類】描述了一種代碼的組織結構形式,它是軟件中對真實世界中問題領域的建模方法。
舉個例子:
就比如咱們現實中修房子,你要修一個「寫字樓」、或者一個「居民樓」、或者一個「商場」,你就得分別找到修「寫字樓」、「居民樓」、「商場」的【設計藍圖】。
可是設計藍圖只是一個建築計劃,並非真正的建築。要想在真實世界實現這個建築,就得由建築工人將設計藍圖的各種特性(好比長寬高、功能)【複製】到現實世界來。
這裏的【設計藍圖】就是【類】,【複製】的過程就是【實例化】,【實例】就是【對象】。
類的內部一般有一個同名的構造方法,咱們設想下,它的僞代碼就多是這樣的:
class Mall { // 「商場」類
Mall( num ){ // 同名構造方法
garage = num // 地下車庫數量
}
shop( goods ) { // 買東西
output( "We can buy: ", goods )
}
}
// 構造函數大多須要用 new 來調,這樣語言引擎才知道你想要構造一個新的類實例。
vanke = new Mall(1) // vanke 有 1 個地下車庫
vanke.shop("KFC") // "We can buy: KFC"
複製代碼
java 是典型的面嚮對象語言。基於「類」,咱們再經過如下一段 java 代碼來看看對繼承和多態的理解。
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(); //嘎嘎嘎
}
複製代碼
雞和鴨都屬於動物分類,均可以發出叫聲(繼承),可是它們卻能夠發出不一樣的叫聲(多態),很容易理解。
繼承可使子類得到父類的所有功能; 多態可使程序有良好的擴展;
回想下:在 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());
複製代碼
這裏既沒用到繼承,也沒用到多態。這樣【寫判斷】是代碼「不清爽」的罪魁禍首!
在 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("嘎嘎嘎")
}
}
}
複製代碼
像這種函數嵌套調用是很常見的。沒有看到繼承,也沒有看到多態,甚至都沒有看到最根本的「類」?!
(實際上,每一個函數都是一個 Function 對象。按照最開始定義所述,對象是類的實例,因此也是能在函數中看到「類」的!)
在 JavaScript 中,函數成了第一等公民! 函數彷佛什麼都能作!它能夠返回一個對象,能夠賦值給一個變量,能夠做爲數組項,能夠做爲對象的一個屬性......
但這明顯不是「類的設計模式」吧!
「類的設計模式」 意味着對【設計藍圖】的【複製】,在 JS 各類函數調用的場景下基本看不到它的痕跡。
其實,衆所周知,JS 也是能作到【繼承】和【多態】的!只不過它不是經過類複製的方式,而是經過原型鏈委託的方式!
一圖看懂原型鏈?
看不懂?不要緊,記住這兩句話再來看:
原來,JS 不是經過在類裏面寫同名構造函數的方式來進一步實現的實例化,它的構造函數在原型上!這種更加奇特的代碼服用機制有異於經典類的代碼複用體系。
這裏再附一個經典問題?JS new 操做會發生什麼?
會是像類那樣進行復制嗎?
答案是否認的!
JS 訪問一個對象的屬性或方法的時候,先在對象自己中查找,若是找不到,則到原型中查找,若是仍是找不到,則進一步在原型的原型中查找,一直到原型鏈的最末端。複製不是它所作的,這種查找的方式纔是!對象之間的關係更像是一種委託關係,就像找東西,你在我這找不到?就到有委託關係的其它人那裏找找看,再找不到,就到委託委託關係的人那裏找......直至盡頭,最後還找不到,指向 null。
因此:JavaScript 和麪向對象的語言不一樣,它並無類來做爲對象的抽象模式或者設計藍圖。JavaScript 中只有對象,對象直接定義本身的行爲。對象之間的關係是委託關係,這是一種極其強大的設計模式。在你的腦海中對象並非按照父類到子類的關係垂直組織的,而是經過任意方向的委託關聯並排組織的!
不過你也能夠經過這種委託的關係來模擬經典的面向對象體系:類、繼承、多態。但「類」設計模式只是一種可選的設計模式,你能夠模擬,也能夠不模擬!
現實是 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 實現了繼承和多態。
然而,這只是語法糖的陷阱!JS 沒有類,沒有複製,它的機制是「委託」。
class 並不會像傳統面向類的語言同樣在申明時做靜態複製的行爲,若是你有意或者無心修改了父類,那子類也會收到影響。
舉例:
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 後,它就變成了一個(將來會被實例化的)東西的 靜態定義。你會完全忽略 Class 是一個對象,是一個具體的能夠直接交互的東西。固然,它還有其它細節問題,好比屬性覆蓋方法、super 綁定的問題,有興趣自行了解。
總地來講,ES6 的 class 想假裝成一種很好的語法問題的解決方案,可是實際上卻讓問題更難解決並且讓 JavaScript 更加難以理解。 —— 《你不知道的 JavaScript》
「類設計模式」的構造函數掛在同名的類裏面,類的繼承意味着複製,多態意味着複製 + 自定義。
「原型設計模式」的構造函數掛在原型上,原型的查找是一種自下而上的委託關係。
「類設計模式」的類定義以後就不支持修改。
「原型設計模式」講究的是一種動態性,任何對象的定義均可以修改,這和 JavaScript 做爲腳本語言所需的動態十分契合!
你能夠用「原型設計模式」來模擬「類設計模式」,可是這大機率是得不償失的。
最後,若是再被問道:JavaScript 是面嚮對象語言嗎?
若是這篇文章看懂了,就能夠圍繞:「類設計模式」和「原型設計模式」來吹了。
若是本文沒有看懂,就把下面的標答背下來吧......
關注公衆號《掘金安東尼》,持續輸出ing!!!
最近有點疲於寫業務代碼😭......誰能支支招?