面向对象一文搞定,终生难忘(面向对象心得)
551
2022-05-30
目的
在对象之间建立关系和共享代码的方法,扩展和改进既有的对象的方法。
概念
javaScript不是基于类的面向对象系统(即用类来产生对象,JavaScript根本没有类),而是基于原型模型,对象可继承和扩展其他对象(即原型对象)的属性和行为,这种方式我们称之为原型式继承或基于原型的继承,其中其行为和属性被继承的对象称为原型。这样做的目的在于继承既有的属性和方法,同时在对象中添加属性和方法。
对象字面量适合创建少量对象的情况。
对象构造函数适合创建大量一致的对象,在代码上来看,实现了代码重用,但在运行效率上看,创建出来的对象都会产生一个包含属性和方法的副本,而方法的副本是完全没有必要存在这么多的,因此会占用大量内存,影响程序性能。
将对象们共用的方法和属性放到原型中,然后所有对象都基于此原型来创建,就能实现代码共享。此时原型对象只有一个,不会产生不必要的对象副本。既节省了大量计算机资源,又提高了程序性能。
使用原型创建对象
在开始前,我们要思考好哪些方法是需要放到原型中去共享,哪些方法和属性需要放在对象实例中。
一般,我们将所有对象都需要的方法放到原型中,把对象实例自身特有的属性和方法放到实例对象中。
我们举个利用汽车对象原型创建汽车对象实例的例子。经分析,所有汽车对象都会有牌子(brand)、控制启动参数(started),还有启动车子(start)、停车(stop)、行驶(drive)等方法,它包含了每个汽车对象都需要的属性和方法。
实际上,每个对象的属性都可能会变化,不太应该放在原型中,但我们暂时这样,顺便可以讲点别的知识。分析后,汽车原型应该是这样的:
接下来,基于这个原型创建货车对象,而货车对象一般都会有weight(载重),height(高),goods(货物)这些属性和卸货(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属性:
小知识:
在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
最后给出上述的完整代码:
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获取原型对象,然后在其中添加要让每个汽车对象都继承的属性和方法即可。**在这里我们需要的是一个继承汽车原型的喷水车原型对象。为此,我们必须创建一个继承汽车原型的汽车对象,因为假如不创建的话,那么汽车原型对象就不会存在,再亲自动手建立关联。
喷水车原型如下:
创建原型链的步骤如下:
第一步创建继承了汽车原型的对象:
对对象实例的唯一要求就是它必须继承了汽车原型。
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
至此,原型链也讲完了。其实就是原型对象换成了基于上一个原型对象创建的对象实例。
下面给出完整的代码:
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
给一个运行实例:
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中,一切几乎皆是对象,包括函数、数组和众多的内置对象和自己创建的自定义对象。
谢谢阅读。
AI JavaScript 交通智能体
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。