- JavaScript基於原型的對象機制
- JavaScript原型上的哪些事
一、JavaScript基於原型的對象機制
JavaScript對象是基於原型的面向對象機制。在一定程度上js基於原型的對象機制依然維持了類的基本特征:抽象、封裝、繼承、多態。面向類的設計模式:實例化、繼承、多態,這些無法直接對應到JavaScript的對象機制。與強類型語言的類相對應的是JavaScript的原型,所以,只能是基於原型來模擬實現類的設計模式。
為了便於理解,這里采用了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);
1.1類設計模式與JavaScript中的類(類的new指令創建對象的設計模式):ES6中的Class
在很多時候我們並不把類看作做一種設計模式,更多的喜歡使用抽象、繼承、多態這種它本身具備的特性來描述它,但是類的本質核心功能就是用來創建對象,在三大類設計模式創建型模式、結構型模式、行為型模式中,類設計模式必然就是創建型模式。
常見的創建型模式比如迭代器模式、觀察者模式、工廠模式、單例模式這些也都可以說是類設計模式的的高級設計模式。
創建型模式提供一種創建對象的同時隱藏創建邏輯的方式,而不是使用new運算符直接實例化對象。這使得程序在判斷針對某個給定實例需要創建那些對象時更加靈活。
不使用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指令實現)。
雖然,可以通過Function和new指令可以實現對象初識化,但是Function是函數並不是類。這與類的設計模式還是有一些差別,在ES6中提供了Class語法來填補了類的設計模式的缺陷,但是JavaScript中的對象實例化本質上還是基於Function來實現的,Class只是語法糖。
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中已經超出了對象實例化的范疇,而且通常大家也不願意這么做。
繼承特性中有必要了解的幾個概念:
- 私有屬性與私有方法:類自身內部的屬性和方法,不能被子類、類的實例對象、子類的實例對象繼承,甚至不能通過類名引用的方式使用,而是只能在類的內部使用的屬性。
- 靜態屬性與靜態方法:類的屬性和方法,可以被子類繼承,不能被類的實例對象、子類的實例對象繼承,只能被類和子類直接訪問和使用。
- 公有屬性與共有方法:所有通過類和子類構造的對象都會(繼承)生成的對象的屬性和對象的方法,並且每個對象基於構造時傳入的初始參數形成自己獨有的屬性值。
注:私有、靜態並不包含常量的意思,當然這兩種屬性我們通常喜歡構建成不可寫的屬性,但是私有和靜態這兩個概念並不討論修改屬性值的問題,私有和靜態只是討論屬性繼承問題,當然私有屬性還有一個關鍵的特點就是不能被類自身在類的外部引用,只能在類的內部使用。

最后,這里說明一點公有屬性從類的設計模式來說是用來構造對象使用的,而非給類直接使用的。ES6中的Class機制提供了靜態屬性的實現和繼承方式,但沒有提供私有屬性的實現方式。下面是ES5與ES6實現繼承的示例,這里並不討論它們的實現及,如果需要了解它們的實現機制請了解下一篇博客,而且因為ES6中並沒有提供私有屬性的機制,示例中也不會擴展,詳細了解下一篇博客:
關於私有屬性可以了解:https://juejin.im/post/5c25faf3f265da61380f4b17
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的代碼是一堆面條代碼,實際上可以封裝,讓結構更清晰,但是這不是這篇博客主要內容,這篇博客重要在於解析清除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的類設計模式全部內容。
二、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 }());
