深刻JavaScript對象(Object)與類(class),詳細瞭解類、原型

  • JavaScript基於原型的對象機制
  • JavaScript原型上的哪些事

 1、JavaScript基於原型的對象機制

JavaScript對象是基於原型的面向對象機制。在必定程度上js基於原型的對象機制依然維持了類的基本特徵:抽象、封裝、繼承、多態。面向類的設計模式:實例化、繼承、多態,這些沒法直接對應到JavaScript的對象機制。與強類型語言的類相對應的是JavaScript的原型,因此,只能是基於原型來模擬實現類的設計模式。html

爲了便於理解,這裏採用了Function構造函數及對象原型鏈的方式模擬汽車構造函數、小型客車類、配置構建五座小型客車對象:設計模式

 1 //汽車構造函數
 2 function Car(type,purpose,modelNumber){  3     this.type = type;         //汽車類型 --如:客車、卡車
 4     this.purpose = purpose;    //用途 --如:載客、載貨、越野
 5     this.modelNumber = modelNumber; //型號 --如:小型客車、中型客車、小型貨車、掛載式貨車
 6     switch(modelNumber){  7         case"passengerCar":  8             this[modelNumber] = PassengerCar;  9             PassengerCar.prototype = this; 10             break; 11  } 12     return this[modelNumber]; 13 } 14 //小型客車構造函數
15 function PassengerCar(brand,wheelHub,seat,engine){ 16     this.brand = brand; 17     this.wheelHub={        //配置輪轂
18         wheelHubCount:wheelHub.wheelHubCount,     //輪轂數量 --如:4,6,8
19         wheelHubTexture:wheelHub.wheelHubTexture,//輪轂材質 --如:鋁合金
20         wheelSpecification:wheelHub.wheelSpecification, //輪胎規格 --如:18,19,20英寸
21         tyreShoeType:wheelHub.tyreShoeType,        //輪胎類型 --如:真空胎,實心胎
22         tyreShoeBrand:wheelHub.tyreShoeBrand        //輪胎品牌 --如:米其林
23  }; 24     this.seat = {    //配置座椅
25         seatCount:seat.seatCount,        //座椅個數 --如:2,4,5,7,9
26         seatTexture:seat.seatTexture    //座椅材質 --如:真皮,仿皮,
27  }; 28     this.engine = { //配置發動機
29         engineBrand:engine.engineBrand,    //發動機品牌
30         engineModelNumber:engine.engineModelNumber //發動機型號
31  } 32 } 33 //建立小型客車類
34 var PassengerCarClass = new Car("小型客車","載客","passengerCar"); 35 // 實例化五座小型客車
36 // 五座小型客車輪轂配置
37 var fivePassengerCarWheelHub = { 38     wheelHubCount:4,     //輪轂數量 --如:4,6,8
39     wheelHubTexture:"鋁合金",//輪轂材質 --如:鋁合金
40     wheelSpecification:"19", //輪胎規格 --如:18,19,20英寸
41     tyreShoeType:"真空胎",        //輪胎類型 --如:真空胎,實心胎
42     tyreShoeBrand:"米其林"
43 } 44 // 五座小型客車發動機配置
45 var fivePassengerCarEngine = { 46     engineBrand:"創馳藍天",    //發動機品牌
47     engineModelNumber:"SKYACTIV-G" //發動機型號
48 } 49 // 五座小型客車座椅配置
50 var fivePassengerCarSeat = { 51     seatCount:5,        //座椅個數
52     seatTexture:"真皮"    //座椅材質
53 } 54 //構建五座小型客車對象
55 var fivePassengerCar = new PassengerCarClass("馬自達",fivePassengerCarWheelHub,fivePassengerCarSeat,fivePassengerCarEngine);
View Code

 1.1類設計模式與JavaScript中的類(類的new指令建立對象的設計模式):ES6中的Class

在不少時候咱們並不把類看做作一種設計模式,更多的喜歡使用抽象、繼承、多態這種它自己具有的特性來描述它,可是類的本質核心功能就是用來建立對象,在三大類設計模式建立型模式、結構型模式、行爲型模式中,類設計模式必然就是建立型模式。數組

常見的建立型模式好比迭代器模式、觀察者模式、工廠模式、單例模式這些也均可以說是類設計模式的的高級設計模式。dom

建立型模式提供一種建立對象的同時隱藏建立邏輯的方式,而不是使用new運算符直接實例化對象。這使得程序在判斷針對某個給定實例須要建立那些對象時更加靈活。ide

不使用new運算符建立對象在JavaScript中建立對象好像有些困難,可是不表明作不到:函數

 1 // 不使用new命令實現js類的設計模式
 2 var Foo = {  3     init:function(who){  4         this.me = who;  5  },  6     identify:function(){  7         return "I am " + this.me;  8  }  9 }; 10 var Bar = Object.create(Foo); //建立一個空對象,將對象原型指向Foo
11 Bar.speak = function(){ 12     console.log("Hello," + this.identify() + "."); 13 }; 14 var b1 = Object.create(Bar);    //建立b1對象
15 var b2 = Object.create(Bar);    //建立b2對象
16 b1.init("b1");    //b1初始化對象參數
17 b2.init("b2");    //b2初始化對象參數
18 
19 b1.speak(); //Hello,I am b1.
20 b2.speak(); //Hello,I am b1.

可是,類與建立型模式仍是有些區別,類建立對象時是須要使用new指令的,同時完成傳參實現對象初始化。在上面的示例須要先使用Object.create(Obj)建立對象,而後使用init方法來實現初始化。這一點JavaScript經過Function和new指令而且能夠傳參實現初始化(摺疊的汽車對象構造採用Function的new指令實現)。post

雖然,能夠經過Function和new指令能夠實現對象初識化,可是Function是函數並非類。這與類的設計模式仍是有一些差異,在ES6中提供了Class語法來填補了類的設計模式的缺陷,可是JavaScript中的對象實例化本質上仍是基於Function來實現的,Class只是語法糖。this

 1 //ES6構造函數
 2 class C{  3  constructor(name){  4         this.name = name;  5         this.num = Math.random();  6  }  7  rand(){  8         console.log(this.name + " Random: " + this.num);  9  } 10 } 11 var c1 = new C("他鄉踏雪"); //建立對象而且傳參初識化
12 c1.rand(); //他鄉踏雪 Random: 0.3835790827213281

1.2類的繼承

在類的設計模式中,實例化時是將父類中全部的屬性與方法複製一份到子類或對象上,這種行爲也叫作繼承。可是這種類實例化對象的繼承設計模式在JavaScript中不能被實現,採用深度複製固然是能作獲得,但這在JavaScript中已經超出了對象實例化的範疇,並且一般你們也不肯意這麼作。spa

繼承特性中有必要了解的幾個概念:

  • 私有屬性與私有方法:類自身內部的屬性和方法,不能被子類、類的實例對象、子類的實例對象繼承,甚至不能經過類名引用的方式使用,而是隻能在類的內部使用的屬性。
  • 靜態屬性與靜態方法:類的屬性和方法,能夠被子類繼承,不能被類的實例對象、子類的實例對象繼承,只能被類和子類直接訪問和使用。
  • 公有屬性與共有方法:全部經過類和子類構造的對象都會(繼承)生成的對象的屬性和對象的方法,而且每一個對象基於構造時傳入的初始參數造成本身獨有的屬性值。

注:私有、靜態並不包含常量的意思,固然這兩種屬性咱們一般喜歡構建成不可寫的屬性,可是私有和靜態這兩個概念並不討論修改屬性值的問題,私有和靜態只是討論屬性繼承問題,固然私有屬性還有一個關鍵的特色就是不能被類自身在類的外部引用,只能在類的內部使用。prototype

最後,這裏說明一點公有屬性從類的設計模式來講是用來構造對象使用的,而非給類直接使用的。ES6中的Class機制提供了靜態屬性的實現和繼承方式,但沒有提供私有屬性的實現方式。下面是ES5與ES6實現繼承的示例,這裏並不討論它們的實現及,若是須要了解它們的實現機制請了解下一篇博客,並且由於ES6中並無提供私有屬性的機制,示例中也不會擴展,詳細瞭解下一篇博客:

關於私有屬性能夠了解:http://www.javashuo.com/article/p-gurbkxdc-hm.html

 1 // ES6中class實現類與對象的繼承示例 (屬性名沒有根據示圖來實現,由於這裏沒有實現私有屬性)
 2 class Foo{  3     static a ; //靜態屬性a
 4     static b = "b" //靜態屬性b
 5     static c(){ //靜態方法C
 6         console.log(this.a,this.b);  7  }  8     constructor(name,age){     //構造函數
 9         this.name = name;    //定義公共屬性name
10         this.age = age;        //定義公共屬性age
11  } 12  describe (){ 13         console.log("我是" + this.name + ",我今年" + this.age + "."); 14  } 15 } 16 class Bar extends Foo{ 17  constructor(name,age,tool,comeTrue){ 18         super(name,age);     //實現繼承,構造Foo實例指向Bar.prototype
19         this.tool = tool;    //添加自身的公共屬性
20         this.comeTrue = comeTrue;    //添加自身的公共屬性
21  } 22  toolFu(){ 23         console.log("我有" + this.tool + ",能夠用來" + this.comeTrue); 24  } 25 } 26 Foo.a = 10; //Foo類給自身的靜態屬性a賦值
27 Bar.a = 20; //Bar類給繼承來靜態屬性a賦值
28 Foo.c(); //10 "b" //Foo調用自身的靜態方法
29 Bar.c(); //20 "b" //Bar調用繼承的靜態方法
30 let obj = new Bar("小明",6,"畫筆","畫畫"); //實例化Bar對象
31 let fObj = new Foo("小張",5);   //實例化Foo對象
32 obj.describe(); //我是小明,我今年6.
33 fObj.describe(); //我是小張,我今年5.
34 obj.toolFu(); //我有畫筆,能夠用來畫畫

經過上面ES6中的Class示例展現了JavaScript的繼承實現,可是前面說了,JavaScript中不具有類的實際設計模式,即使是Class語法糖也仍是基於Function和new機制來完成的,接着下面就是用ES5的語法來實現上面示例代碼的同等功能(僅僅實現同等功能,不模擬Class實現,在解析class博客中再寫):

 1 // ES5中Function基於構造與原型實現類與對象的繼承示例
 2 function Foo(name,age){ //聲明Foo構造函數,相似Foo類
 3     this.name = name;  4     this.age = age;  5     this.describe = function describe(){  6         console.log("我是" + this.name + ",我今年" + this.age + ".");  7  }  8 }  9 Object.defineProperty(Foo,"a",{ //配置靜態屬性a
10     value:undefined,     //固然也能夠直接採用Foo.a的字面量來實現
11     writable:true, 12     configurable:true, 13     enumerable:true
14 }); 15 Object.defineProperty(Foo,"b",{ //配置靜態屬性b
16     get:function(){ 17         return "b"; 18  }, 19     configurable:true, 20     enumerable:true     //雖說可枚舉屬性描述符不寫默認爲true,可是不寫出現不能枚舉的狀況
21 }); 22 Object.defineProperty(Foo,"c",{ //配置靜態方法c
23     value:function(){ 24         console.log(this.a,this.b); 25  }, 26     configurable:true, 27     enumerable:true
28 }); 29 function Bar(name,age,tool,comeTrue){ //聲明Bar構造函數,相似Bar類
30     this.__proto__ = new Foo(name,age); 31     this.tool = tool; 32     this.comeTrue = comeTrue; 33     this.toolFu = function(){ 34         console.log("我有" + this.tool + ",能夠用來" + this.comeTrue); 35  } 36 } 37 for(var key in Foo){ 38     if(!Bar.propertyIsEnumerable(key)){ 39         Bar[key] = Foo[key]; 40  } 41 } 42 Foo.a = 10; //Foo類給自身的靜態屬性a賦值
43 Bar.a = 20; //Bar類給繼承來靜態屬性a賦值
44 Foo.c(); //10 "b" //Foo調用自身的靜態方法
45 Bar.c(); //20 "b" //Bar調用繼承的靜態方法
46 let obj = new Bar("小明",6,"畫筆","畫畫"); //實例化Bar對象
47 let fObj = new Foo("小張",5);   //實例化Foo對象
48 obj.describe(); //我是小明,我今年6.
49 fObj.describe(); //我是小張,我今年5.
50 obj.toolFu(); //我有畫筆,能夠用來畫畫
ES5中Function基於構造與原型實現類與對象的繼承示例

上面這個ES5的代碼是一堆麪條代碼,實際上能夠封裝,讓結構更清晰,可是這不是這篇博客主要內容,這篇博客重要在於解析清除JS基於對象原型的實例化機制。

採用上面這種寫法也是爲了鋪墊下一篇博客解析Class語法糖的底層原理。

1.3多態

多態就是重寫父類的函數,這個看起來很簡單的描述,每每在項目中是個很是難以抉擇的部分,好比由多態產生的多重繼承,這種設計對於編寫代碼和理解代碼來講都很是有幫助,可是對於系統執行,特別是JavaScript這個面向過程、基於原型的語言很是糟糕。下面就來看看Class語法中如何實現的多態吧,ES5語法實現多態就不寫了。

 1 //多態 
 2 class Foo{  3  fun(){  4         console.log("我是父級類Foo上的方法");  5  }  6 }  7 class Bar extends Foo{  8  constructor(){  9  super(); 10  } 11  fun(){ 12         console.log("我是子類Bar上的方法"); 13  } 14 } 15 class Coo extends Foo{ 16  constructor(){ 17  super(); 18  } 19  fun(){ 20         console.log("我是子類Coo上的方法"); 21  } 22 } 23 var foo = new Foo(); 24 var bar = new Bar(); 25 var coo = new Coo(); 26 foo.fun();    //我是父級類Foo上的方法
27 bar.fun();    //我是子類Bar上的方法
28 coo.fun();    //我是子級類Coo上的方法

以上就是關於JavaScript關於類設計模式的所有內容,或許你會疑惑還有抽象和封裝沒有解析,其實類的設計模式中始終貫徹着抽象與封裝的概念。把行爲本質上相關聯的數據和數據的操做抽離稱爲一個獨立的模塊,自己就是抽象與封裝的過程。而後在前面已經詳細的介紹了JavaScript的繼承與多態的設計方式,可是我一直在規避進入一個話題,這個話題就是JavaScript的原型鏈。若是將這個JavaScript語言本質特性放到前面的類模式設計中去一塊兒描述的話,那是沒法想象的漿糊,由於原型幾乎貫穿了JavaScript的類設計模式所有內容。

 2、JavaScript原型上的哪些事

  • 對象原型是什麼?
  • 對象原型如何產生?
  • 對象原型與繼承模式、聖盃模式

 2.1對象原型[[prototype]]

JavaScript對象上有一個特性的[[prototype]]內置屬性,這個屬性也就是對象的原型。直接聲明的對象字面量或者Object構造的對象,其原型都指向Object.prototype。再往Object.prototype的上層就是null,這也是全部對象訪問屬性的終點。

可能經過上面的一段說明,仍是不清楚[[prototype]]是什麼,本質上prototype也是個對象,當一個對象訪問屬性時,先從自身的屬性中查找,若是自身沒有該屬性,就逐級向原型鏈上查找,訪問到Object.peototype的上層時發現其爲null時結束。

 

思考下面的代碼:

1 var obj = { 2     a:10
3 }; 4 var obj1 = Object.create(obj); 5 obj1.a++; 6 console.log(obj.a);//10
7 console.log(obj1.a);//11

上面這段示例代碼揭示了對象對原型屬性有遮蔽效果,這種遮蔽效果實際上就是對象在自身複製了一份對象屬性描述,這種複製發生在原型屬性訪問時,但不是全部的屬性訪問都會發生遮蔽複製,具體會出現三種狀況:

  • 對象訪問原型屬性,該原型屬性沒有被標記爲只讀(witable:true),這時對象就會在自身添加當前原型屬性的屬性描述符,發生遮蔽。
  • 對象訪問原型屬性,該原型屬性被標記爲只讀(witable:false),屬性沒法修改原型屬性,也不會在自生添加屬性描述符,不會發生遮蔽。若是是在嚴格模式下,對只讀屬性作寫入操做會報錯。
  • 對象訪問原型屬性,該屬性的屬性的讀寫描述符是setter和getter時,屬性根據setter在原型上修改屬性值,不會在自身添加屬性描述符,不會發生遮蔽。

可是有種狀況,即使是在原型屬性witable爲true的狀況下,對象會複製原型的屬性描述符,可是依然沒法遮蔽:

1 var obj = { 2     a:[1,2,3] 3 }; 4 var obj1 = Object.create(obj); 5 obj1["a"].push(4); 6 console.log(obj.a);//[1,2,3,4]
7 console.log(obj1.a);//[1,2,3,4]

這是由於即使對象複製了屬性描述符,但屬性描述符中的value最終都指向了一個引用值的數組。(關於屬性描述符能夠了解:初識JavaScript對象)。

2.2對象原型如何產生?

對象原型是由構造函數的prototype賦給對象的,來源於Function.prototype。

關於對象原型的產生可能會有幾個疑問:

  • 對象字面量形式的[[prototype]]怎麼產生?
  • 對象爲何不能直接使用obj.prototype的字面量方式賦值?賦值會發什麼?
  • 如何修改對象原型?
 1 var obj = {  2     a:2
 3 }  4 function Foo(){  5     this.b = 10
 6 }  7 Foo.prototype = obj; //將構造函數Foo的prototype指向obj
 8 var obj1 = new Foo(); //經過構造函數Foo生成obj1,實質上由Foo執行時產生的VO中的this生成,函數經過new執行對象建立時,this指向變量對象上的this
 9 console.log(obj1.a);//2 //a的屬性自來原型obj
10 console.log(obj1.b);//10 

經過示圖來了解構造函數的實際構建過程:

在前面對象原型的介紹中介紹過,對象原型[[prototype]]是內置屬性,是不能修改的,若是對作這樣的字面量修改:obj1.prototype = obj;只會在對象上顯式的添加一個prototype的屬性,並不能真正的修改到ojb1的原型指向。可是咱們知道obj1原型[[prototype]]指向的是Foo.prototype,函數能夠顯式的修改[[prototype]]的指向,因此示例中修改Foo.prototype就實現了obj1的原型的修改。

若是要深究爲何不能顯式的修改對象的prototype呢?其實對象上的原型屬性名實際上並非「prototype」,而是「__proto__」,因此,上面的示例代碼能夠這樣寫:

 1 var obj = {  2     a:2
 3 }  4 function Obj(){  5     this.__proto__ = obj; //構造函數內部經過this.__proto__修改原型指向
 6     this.b = 10
 7 }  8 var obj1 = new Obj();  9 console.log(obj1.a);//2
10 console.log(obj1.b);//10

這種__proto__屬性命名也被稱爲非標準命名方式,這種方式命名的屬性名不會被for in枚舉,一般也稱爲內部屬性。實現原理(用於原理說明,實際執行報錯):

 1 var obj = {  2     a:2
 3 }  4 // 對象原型讀寫原理,可是不能經過字面量的方式實現,下面這種寫法非法
 5 var ojb1 = {  6  set __proto__(value){  7         this.__proto__ = value;  8  },  9  get __proto__(){ 10         return this.__proto__; 11  }, 12     b:10
13 } 14 ojb1.__proto__ = obj;

最後說明一點,每一個對象上都會有constructor這個屬性,這個屬性指向了構造對象的構造函數,可是這個屬性並非自身的構造函數,而是原型上的,也就是說constructor指向的是原型的構造函數:

 1 function ObjFun(name){  2     this.name = name;  3 }  4 function ObjFoo(name,age){  5     fun.prototype = new ObjFun(name);  6     function fun(age){  7         this.age = age;  8  }  9     return new fun(age); 10 } 11 var obj1 = new ObjFoo("小明") 12 var obj2 = ObjFoo("小紅",18); 13 
14 console.log(obj1.name + "--" + obj1.age + "--" + obj1.constructor); //指向ObjFun
15 console.log(obj2.name + "--" + obj2.age + "--" + obj2.constructor); //指向ObjFun

示例中obj2的constructor爲何是ObjFun其實很簡單,由於obj2對象自己沒有constructor方法,而是來源於fun.prototype.constructor,可是fun的prototype指向了ObjFun的實例,因此最後obj2是經過ObjFun的實例獲取到的constructor。

2.3對象原型與繼承模式、聖盃模式

 

上圖使用這篇博客開篇第一個示例的代碼案例,分析了構造函數構造來實現公有屬性繼承,會出現數據冗餘。這種閉端能夠用公有原型的方式來解決:

2.3.1:公有原型

 公有原型就是兩個構造函數共同使用一個prototype對象,它們構造的全部對象的原型都是同一個,瞭解下面的代碼實現:

 1 Father.prototype.lastName = "Deng";  2 function Father(){}  3 function Son(){}  4 function inherit(Targe,Origin){ //實現共用原型的方法
 5     Targe.prototype = Origin.prototype; //將Origin的原型做爲公有原型
 6 }  7 inherit(Son,Father);//實現原型共享,這裏的公有原型對象是Father.prototype
 8 var son = new Son();  9 var father = new Father(); 10 console.log(son.lastName);//Deng
11 console.log(father.lastName);//Deng

可是,公有原型的繼承方式相對構造函數的方式實現,構造的對象沒有各自獨有的原型,不方便拓展各自獨有的屬性。其優勢就是能夠實現任意兩個構造函數實現公有原型。

2.3.2:聖盃模式

聖盃模式就是在公有原型的基礎上,實現了繼承方的獨立的原型,供各本身構造的對象使用,繼承方修改原型不會影響被繼承的原型。(可是被繼承方修改原型會影響繼承方)

1 function inherit(Target,Origin){ 2     function F(){}; 3     F.prototype = Origin.prototype; 4     Target.prototype = new F(); 5     Target.prototype.constructor = Target; 6     Target.prototype.uber = Origin.prototype; 7 }

其實聖盃模式是讓繼承方的構造函數的原型指向了一個空對象,而構造這個空對象的構造函數的原型指向了被繼承方的原型,這時候繼承方的實例化對象擴展屬性就是在空對象擴展,繼承方在原型擴展屬性不會影響被繼承方,可是聖盃模式中的被繼承方在原型上擴展方法和屬性依然能被繼承方式用。畢竟聖盃模式原本的設計就是被保持繼承關係的,而並不是前面示圖那樣保持公有原型,各自擴展。真正的聖盃模式:

 1 //YUI3雅虎
 2 var inherit = (function(){  3     function F(){};//將F做爲私有化變量
 4     return function(Target,Origin){  5         F.prototype = Origin.prototype;  6         Target.prototype = new F();  7         Target.prototype.constructor = Target;  8         Target.prototype.uber = Origin.prototype;  9  } 10 }());
雅虎YUI3實現的聖盃模式
相關文章
相關標籤/搜索