深刻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实现的圣杯模式
相关文章
相关标签/搜索