JavaScript最后的秘密——使用原型创建对象

网友投稿 564 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

最后给出上述的完整代码:

hello

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

至此,原型链也讲完了。其实就是原型对象换成了基于上一个原型对象创建的对象实例。

下面给出完整的代码:

hello

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

给一个运行实例:

heloo world ~

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

JavaScript最后的秘密——使用原型创建对象

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小时内删除侵权内容。

上一篇:神经网络常用卷积总结
下一篇:Java面向对象知识点拆分(一)
相关文章