JavaScript最后的秘密——使用原型创建对象
目的
在对象之间建立关系和共享代码的方法,扩展和改进既有的对象的方法。
概念
JavaScript不是基于类的面向对象系统(即用类来产生对象,JavaScript根本没有类),而是基于原型模型,对象可继承和扩展其他对象(即原型对象)的属性和行为,这种方式我们称之为原型式继承或基于原型的继承,其中其行为和属性被继承的对象称为原型。这样做的目的在于继承既有的属性和方法,同时在对象中添加属性和方法。
对象字面量适合创建少量对象的情况。
对象构造函数适合创建大量一致的对象,在代码上来看,实现了代码重用,但在运行效率上看,创建出来的对象都会产生一个包含属性和方法的副本,而方法的副本是完全没有必要存在这么多的,因此会占用大量内存,影响程序性能。
将对象们共用的方法和属性放到原型中,然后所有对象都基于此原型来创建,就能实现代码共享。此时原型对象只有一个,不会产生不必要的对象副本。既节省了大量计算机资源,又提高了程序性能。
使用原型创建对象
在开始前,我们要思考好哪些方法是需要放到原型中去共享,哪些方法和属性需要放在对象实例中。
一般,我们将所有对象都需要的方法放到原型中,把对象实例自身特有的属性和方法放到实例对象中。
我们举个利用汽车对象原型创建汽车对象实例的例子。经分析,所有汽车对象都会有牌子(brand)、控制启动参数(started),还有启动车子(start)、停车(stop)、行驶(drive)等方法,它包含了每个汽车对象都需要的属性和方法。
实际上,每个对象的属性都可能会变化,不太应该放在原型中,但我们暂时这样,顺便可以讲点别的知识。分析后,汽车原型应该是这样的:
汽车原型 |
---|
brand:“BMW” //牌子 started:false//是否已启动,false 没有启动 |
start()//启动 stop()//停车 drive()//开行 |
接下来,基于这个原型创建货车对象,而货车对象一般都会有weight(载重),height(高),goods(货物)这些属性和卸货(unload)这个方法。所以我们的货车对象看起来是这样的:
货车对象实例 |
---|
weight:56 //载重 height:2//高 goods:“apple”//货物 |
unload()//卸货 |
分析完毕。
一般来说我们应该先建原型,再建对象。但在JavaScript中,要**(1)先创建货车对象的构造函数**,然后**(2)通过函数的属性prototype获得原型对象,然后往原型对象里添加属性和方法**。步骤如下:
第一步,定义货车对象构造函数:
function CarModel(weight,height,goods){ this.weight = weight; this.height = height; this.goods = goods; this.unload = function(){ alert("开始卸货"); }; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
第二步,创建构造函数后,获取汽车原型对象,并设置原型:
CarModel.prototype.brand = "BMW";
CarModel.prototype.started = false;
CarModel.prototype.drive = function(){ //if(CarModel.prototype.started){ if(this.started){//如果实例中没有此属性,就会到原型中找 alert("start start start"); }else{ alert("no!"); }
};
CarModel.prototype.stop = function(){ //CarModel.prototype.started = false; this.started = false;//这将在对象实例中创建属性started
};
CarModel.prototype.start = function(){ //CarModel.prototype.started = true; this.started = true;//这将在对象实例中创建属性started
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
上述例子中之所以不用CarModel.prototype.started而用this.started是因为修改原型中的started以影响到所有对象( 再次说明, 原型中不太应该放属性)。执行this.started = true后,会在对象实例中添加started属性:
货车对象(前) | 货车对象(后) |
---|---|
weight height goods |
weight height goods started |
小知识:
在JavaScript中,函数也是对象,也有属性。对象构造函数里包含属性prototype,这是一个指向原型对象的引用。但是这个原型对象默认包含的属性与方法不多,所以我们要给原型对象添加属性和方法,这通常是在使用构造函数前进行的。
可以通过 CarModel.prototype访问原型对象, 并通过其向原型对象中添加属性和方法 。向原型添加的方法和属性将被所有对象所共用。对象自己特有的方法与属性,则在对象构造函数中添加(这其实也是在对象实例上添加),或者直接在对象实例上添加,如:
var c1 = new CarModel(11,11,"apple1"); c1.seats = 6;
c1.flash = function(){ alert("turn on the flash light");
};
- 1
- 2
- 3
- 4
- 5
如上面的seats属性与flash方法只属于对象cm。其他用对象构造函数创建出的对象是没有的。
第三步,测试:
//c1 c2 c3将会共用原型中的代码
var c1 = new CarModel(11,11,"apple1");
var c2 = new CarModel(22,22,"apple2");
var c3 = new CarModel(33,33,"apple3");
c1.start();
c1.drive();//start start start
c1.stop();
c1.drive();//no!
c1.brand = "MMMM"; //这里的brand已经不是原型中那个brand了,这是我们在c1对象实例创建的brand属性。此语句就是正在创建对象实例变量brand。
alert(c1.hasOwnProperty("brand")); //true,证实c1.brand = "MMMM"赋值语句让brand变成c1对象实例的属性。
alert(c1.brand);//MMMM
alert(c2.brand);//BMW
alert(c2.hasOwnProperty("brand"));//false,说明brand属性来自原型,因为上一条测试语句能访问brand,且此测试语句又说明brand不是c2对象实例的属性,那它只能是来自c2对象的原型。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
到这里为止,我们展示了搭建原型和通过原型创建对象的过程。
注意:原型中的属性与方法都是共用的,没有副本。
继承原型并不意味着必须与它完全相同。在任何情况下,都可以重写原型的属性和方法,为此只需在对象实例中提供它们即可,重写原型中的start方法:
c1.start = function(){
alert("hello Earth");
};
- 1
- 2
- 3
- 4
最后给出上述的完整代码:
<!doctype html>
<html lang="en"> <head><title>hello </title> </head> <body> <script> function CarModel(weight,height,goods){ this.weight = weight; this.height = height; this.goods = goods; this.unload = function(){ alert("开始卸货"); }; } CarModel.prototype.brand = "BMW"; CarModel.prototype.started = false; CarModel.prototype.drive = function(){ //if(CarModel.prototype.started){ if(this.started){ alert("start start start"); }else{ alert("no!"); } }; CarModel.prototype.stop = function(){ //CarModel.prototype.started = false; this.started = false; }; CarModel.prototype.start = function(){ //CarModel.prototype.started = true; this.started = true; }; var c1 = new CarModel(11,11,"apple1"); var c2 = new CarModel(22,22,"apple2"); var c3 = new CarModel(33,33,"apple3"); c1.start(); alert(CarModel.prototype.started); c1.drive(); c1.stop(); c1.drive(); c1.brand = "MMMM"; alert(c2.brand); alert(c1.brand); alert(c1.hasOwnProperty("brand")); alert(c2.hasOwnProperty("brand")); </script> </body>
</html>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
以上的货车对象是在汽车原型的基础上创建的。
建立原型链
对像不仅可能继承一个原型,还可以继承一个原型链。就像B原型继承A原型,C原型继承B原型,D原型又继承C原型,这样形成的一条链条,例如D就同时拥有A、B、C等原型的属性与方法。
我们通过一个基于喷水车原型创建喷水车对象实例的例子来说明。提醒一下,前面我们已经有了一个汽车原型了。
分析:(1)我们决不能通过修改上面货车构造函数CarModel来适应我们的变化,因为这一改就会影响到其他货车对象,这硬生生给货车加上喷水车的属性和方法,显然也不合理。(2)单独再创建一个喷水车构造函数的话,那么汽车原型的代码就要在喷水车原型中重新设置。
所以最好的做法是建个喷水车原型,然后再让其继承汽车原型,这样汽车原型这部分代码就不用重新在喷水车原型中设置。
**我们在创建汽车原型时,只需直接通过构造函数CarModel的属性prototype获取原型对象,然后在其中添加要让每个汽车对象都继承的属性和方法即可。**在这里我们需要的是一个继承汽车原型的喷水车原型对象。为此,我们必须创建一个继承汽车原型的汽车对象,因为假如不创建的话,那么汽车原型对象就不会存在,再亲自动手建立关联。
喷水车原型如下:
喷水车原型 |
---|
volumn:12 //水量 |
sprayWater()//喷水功能 |
创建原型链的步骤如下:
第一步创建继承了汽车原型的对象:
对对象实例的唯一要求就是它必须继承了汽车原型。
var car = new CarModel();
- 1
第二步创建喷水车对象构造函数:
function SprayCarModel(weight,height,goods,name,handler){ CarModel.call(this,weight,height,goods); this.name = name; this.handler = handler;
}
- 1
- 2
- 3
- 4
- 5
说明:创建继承另一个原型的构造函数时,都不应该重复既有的代码,下面就重复了货车对象构造函数的代码:
function SprayCarModel(weight,height,goods,name,handler){ this.weight = weight; this.height = height; this.goods = goods; this.name = name; this.handler = handler;
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
解决办法:
CarModel.call(this,weight,height,goods);
- 1
它其实是调用CarModel对象构造函数,给当前new SprayCarModel出来的对象this赋值。传当前对象的引用this过去,再调用CarModel对象构造函数对this进行赋值。
为什么要这样做?
通过第三步我们可知,货车对象实例将变成喷水车原型,而原型对象是共用的,所以将weight、height、goods放在喷水车对象实例中会更好,如果使用原型中的话,那么对象之间就会互相影响。共用原型中的方法就不会有这种问题,因为大家都是相同的,但是数据就不是了,各有各的不同。
所以这一条call语句相当于做了以下事情:
this.weight = weight;
this.height = height;
this.goods = goods;
- 1
- 2
- 3
调用对象构造函数是不会产生新对象的,和调用普通函数一样,调用对象构造函数一般都是给对象属性赋值。
只有用运算符new,才会产生新对象,它会先创建一个空对象并将引用赋给this,返回this,然后再调用对象构造函数对对象this进行赋值。由此可见new才会产生新对象,而调用对象构造函数是不会产生新对象的。
所以call一番操作后,喷水车原型中的属性,就根本没有给它们赋值过,因此它们都是未定义的undefined
第三步将新建的继承了汽车原型的对象变成喷水车原型:
SprayCarModel.prototype = car;//将实例car变成SprayCarModel的原型
- 1
注意:另忘了,喷水车原型依然是一个货车对象实例。其实,还可以通过创建一个对象字面量,然后用作原型,如
var d = {
start:function(){},
…
};
SprayCarModel.prototype = d;//SprayCarModel就将继承d中的属性和方法。
第四步向喷水车原型中添加属性和方法:
SprayCarModel.prototype.volume = 11;//设置原型的属性
//设置原型的方法
SprayCarModel.prototype.sprayWater = function(){ alert("spray spray spray");
};
- 1
- 2
- 3
- 4
- 5
第五步,测试:
//创建一个喷水车对象实例 var sprayCar = new SprayCarModel(29999,3,"applepie","AAP",15); alert(sprayCar.hasOwnProperty("weight"));//false alert(sprayCar.weight);//29999,上面说明weight不是实例的属性,本语句说明能访问weight属性,说明weight是在原型中的。 var sprayCar1 = new SprayCarModel(999,3,"applepie","AAP",15); alert(sprayCar1.weight);//999 alert(sprayCar.weight);//29999 alert("weight belongs to sprayCar:"+sprayCar.hasOwnProperty("weight"));//true,说明通过CarModel.call(this,weight,height,goods);已将weight变成了sprayCar对象实例的属性了。height、goods也是如此。 sprayCar.sprayWater();//访问喷水车原型中的sprayWater方法 alert(sprayCar.volumn);//访问喷水车原型中的属性 alert(sprayCar.hasOwnProperty("volumn")); //false,结合上一条语句,说明volumn是在原型中的 alert(sprayCar.brand);//BMW,能访问汽车原型中的属性 sprayCar.drive();//能访问汽车原型中的方法 alert(sprayCar.hasOwnProperty("name"));//喷水车对象实例的属性
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
至此,原型链也讲完了。其实就是原型对象换成了基于上一个原型对象创建的对象实例。
下面给出完整的代码:
<!doctype html>
<html lang="en">
<head><title>hello </title>
</head>
<body>
<script>
function CarModel(weight,height,goods){
this.weight = weight;
this.height = height;
this.goods = goods;
this.unload = function(){ alert("开始卸货");
};
}
CarModel.prototype.brand = "BMW";
CarModel.prototype.started = false;
CarModel.prototype.drive = function(){
//if(CarModel.prototype.started){
if(this.started){ alert("start start start");
}else{ alert("no!");
}
};
CarModel.prototype.stop = function(){
//CarModel.prototype.started = false;
this.started = false;
};
CarModel.prototype.start = function(){
//CarModel.prototype.started = true;
this.started = true;
}; function SprayCarModel(weight,height,goods,name,handler){
CarModel.call(this,weight,height,goods);
this.name = name;
this.handler = handler;
}
var car = new CarModel(10000);
SprayCarModel.prototype = car;
SprayCarModel.prototype.constructor = SprayCarModel;
SprayCarModel.prototype.volumn = 11;
SprayCarModel.prototype.sprayWater = function(){
alert("spray spray spray");
}; var sprayCar = new SprayCarModel(29999,3,"applepie","AAP",15);
sprayCar.toString();
car.toString();
alert(sprayCar.hasOwnProperty("weight"));
alert(sprayCar.weight);
var sprayCar1 = new SprayCarModel(999,3,"applepie","AAP",15);
alert(sprayCar1.weight); alert(sprayCar.weight);
alert("weight belongs to sprayCar:"+sprayCar.hasOwnProperty("weight"));
sprayCar.sprayWater();
alert(sprayCar.volumn);
alert(sprayCar.hasOwnProperty("volumn"));
alert(sprayCar.brand);
sprayCar.drive();
sprayCar.unload();
alert(sprayCar.hasOwnProperty("name"));
console.log("SprayCarModel constructor is:"+sprayCar.constructor);
</script>
</body>
</html>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
原型链中的继承原理
对象调用方法或访问属性时,首先在对象实例里查找,如果查找不到,就会沿继承链上移,在其原型中接着查找。
意外!意外!意外!
console.log("SprayCarModel constructor is:"+sprayCar.constructor);
- 1
输出的结果是:
SprayCarModel constructor is:function CarModel(weight,height,goods){
this.weight = weight;
this.height = height;
this.goods = goods;
}
```
- 1
- 2
- 3
- 4
- 5
- 6
不可能呀,SprayCarModel才是sprayCar对象实例的构造器呀,怎么成了CarModel。原来,我们要显式地把对象构造函数的constructor属性设置为SprayCarModel对象构造器函数。虽然不设置也不会有什么影响,但是最佳实践建议还是设置的好。
显式设置对象构造函数的constructor属性设置为SprayCarModel
SprayCarModel.prototype.constructor = SprayCarModel;
- 1
再看看结果:
console.log("SprayCarModel constructor is:"+sprayCar.constructor);
- 1
输出的结果是:
SprayCarModel constructor is:function SprayCarModel(name,handler){
this.name = name;
this.handler = handler;
}
- 1
- 2
- 3
- 4
这下终于正确了。
总结:
我们创建的每个原型链的终点都是Object。我们创建的任何对象,默认原型都是Object,除非你对其进行了修改。喷水车原型从汽车原型派生出来,汽车原型是从Object派生出来。所有对象都是从Object派生出来的,所以我们创建的每个对象都有原型,该原型默认是Object。当然,你可以将对象的原型设置为其他对象,如喷水车原型是汽车对象实例,无论怎样,所有原型链的终点都是Object。
原型是动态的,只要在原型上作任何修改,就会马上反映到各个对象上去。可以对象实例中重写原型中的方法和属性。
Object实现了很多重要的方法,如hasOwnProperty、toString,它们是javaScript对象系统的核心部分。
我们常常会重写Object原型中的toString方法,如:
TruckModel.prototype.toString = function(){ alert("HDDDDDDDD"); };
var truck = new TruckModel("baobao",true,5000,1.5,"apple");
truck.toString();
- 1
- 2
- 3
- 4
- 5
但不是每个方法都能重写,如以下这些就是不能重写的:
- constructor 表示与原型相关联的构造函数
- hasOwnProperty判断实例是否有此属性,每个对象都有此方法。如果属性不是在对象实例中定义的,但能够访问它,就可以认为它肯定是在原型中定义的。
- isPrototypeOf判断一个对象是否是另一个对象的原型
如car.isPrototypeOf(truck) //true - propertyIsEnumerable用于判断通过迭代对象的所有属性是否可访问指定的属性。
而下面这些方法是可重写:
toString
toLocaleString
valueOf
给一个运行实例:
<!doctype html>
<html lang="en"> <head><title>heloo world</title></head> <body> <script> function CarModel(brand,started){ this.brand = brand; this.started = started; this.start=function(){ this.started = true; }; this.stop=function(){ this.started = false; }; this.drive=function(){ if(started){ alert("I am driving"); }else{ alert("You have not fired you car"); } }; } var car = new CarModel(); function TruckModel(brand,started, weight,height,goods){ CarModel.call(this,brand,started); this.weight = weight; this.height = height; this.goods = goods; this.weigh = function(){ alert("称重55吨"); }; }
TruckModel.prototype = car; TruckModel.prototype.constructor = TruckModel; TruckModel.prototype.toString = function(){ alert("HDDDDDDDD"); }; var truck = new TruckModel("baobao",true,5000,1.5,"apple"); alert("dddd:"+car.isPrototypeOf(truck)); truck.toString(); truck.drive(); TruckModel.prototype.color = "blue"; TruckModel.prototype.addWater = function(){ alert("Please add water,the water box is lack of water!"); }; alert(truck.color); truck.addWater(); truck.sayHi = function(){ alert("hello truck truck"); }; truck.speaker = "TTTTT"; alert(truck.speaker); truck.sayHi(); alert(truck instanceof TruckModel); console.log("truck constructor is:"+truck.constructor); alert(truck.brand); var dd = new TruckModel("mama",false,77,88,"pear"); alert(dd.brand); dd.addWater(); </script> </body>
</html>
~
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
扩展内置对象
其实与上面的一样,通过在原型中添加方法和属性,如扩展String内置对象:
String.prototype.clickme = function(){
alert("you clicked me");
};
- 1
- 2
- 3
其他的依次类推。
最后回顾一下:
- JavaScript对象系统使用原型式继承
- 使用构造函数创建对象实例时,实例包含自己的自定义属性,还有构造函数中方法的副本。
- 给构造函数的原型添加属性后,使用这个构造函数创建的实例都将继承这些属性。
- 通过在原型是中定义属性,可减少对象包含的重复代码。
- 要重写原型中的属性,只需在实例中添加该属性即可。
- 构造函数有默认的原型,可通过函数的属性prototype来访问它。
- 可将你自己创建的对象赋给构造函数的属性prototype
- 使用自定义的原型对象时,务必将原型的属性constructor设置为相应的对象构造函数,以保持一致。
- 给原型添加属性后,继承该原型的所有实例都将立即继承这些属性,即便是以前创建的实例也不例外。
- 归根结底,所有原型和对象都是从Object派生而来的。
- Object包含所有对象都将继承的属性和方法,如toString和hasOwnProperty
- 可給内置对象(如Object和String等)添加属性,也可重写它们的既有属性,但要小心。
- 在JavaScript中,一切几乎皆是对象,包括函数、数组和众多的内置对象和自己创建的自定义对象。
谢谢阅读。
文章来源: blog.csdn.net,作者:WongKyunban,版权归原作者所有,如需转载,请联系作者。
原文链接:blog.csdn.net/weixin_40763897/article/details/88071141
- 点赞
- 收藏
- 关注作者
评论(0)