0
点赞
收藏
分享

微信扫一扫

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("开始卸货");
};

}

第二步,创建构造函数后,获取汽车原型对象,并设置原型

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
};

上述例子中之所以不用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");
};

如上面的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对象的原型。

到这里为止,我们展示了搭建原型和通过原型创建对象的过程。

注意:原型中的属性与方法都是共用的,没有副本。

继承原型并不意味着必须与它完全相同。在任何情况下,都可以重写原型的属性和方法,为此只需在对象实例中提供它们即可,重写原型中的start方法:

c1.start = function(){
alert("hello Earth");
};

最后给出上述的完整代码

<!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>

以上的货车对象是在汽车原型的基础上创建的。

建立原型链

对像不仅可能继承一个原型,还可以继承一个原型链。就像B原型继承A原型,C原型继承B原型,D原型又继承C原型,这样形成的一条链条,例如D就同时拥有A、B、C等原型的属性与方法。
我们通过一个基于喷水车原型创建喷水车对象实例的例子来说明。提醒一下,前面我们已经有了一个汽车原型了。

分析:(1)我们决不能通过修改上面货车构造函数CarModel来适应我们的变化,因为这一改就会影响到其他货车对象,这硬生生给货车加上喷水车的属性和方法,显然也不合理。(2)单独再创建一个喷水车构造函数的话,那么汽车原型的代码就要在喷水车原型中重新设置。

所以最好的做法是建个喷水车原型,然后再让其继承汽车原型,这样汽车原型这部分代码就不用重新在喷水车原型中设置。

**我们在创建汽车原型时,只需直接通过构造函数CarModel的属性prototype获取原型对象,然后在其中添加要让每个汽车对象都继承的属性和方法即可。**在这里我们需要的是一个继承汽车原型的喷水车原型对象。为此,我们必须创建一个继承汽车原型的汽车对象,因为假如不创建的话,那么汽车原型对象就不会存在,再亲自动手建立关联。

喷水车原型如下:

喷水车原型

volumn:12 //水量

sprayWater()//喷水功能

创建原型链的步骤如下:
第一步创建继承了汽车原型的对象:
对对象实例的唯一要求就是它必须继承了汽车原型。

var car = new CarModel();

第二步创建喷水车对象构造函数:

function SprayCarModel(weight,height,goods,name,handler){
CarModel.call(this,weight,height,goods);
this.name = name;
this.handler = handler;
}

说明:创建继承另一个原型的构造函数时,都不应该重复既有的代码,下面就重复了货车对象构造函数的代码:

function SprayCarModel(weight,height,goods,name,handler){
this.weight = weight;
this.height = height;
this.goods = goods;
this.name = name;
this.handler = handler;
};

解决办法:

CarModel.call(this,weight,height,goods);

它其实是调用CarModel对象构造函数,给当前new SprayCarModel出来的对象this赋值。传当前对象的引用this过去,再调用CarModel对象构造函数对this进行赋值。
为什么要这样做?
通过第三步我们可知,货车对象实例将变成喷水车原型,而原型对象是共用的,所以将weight、height、goods放在喷水车对象实例中会更好,如果使用原型中的话,那么对象之间就会互相影响。共用原型中的方法就不会有这种问题,因为大家都是相同的,但是数据就不是了,各有各的不同。

所以这一条call语句相当于做了以下事情:

this.weight = weight;
this.height = height;
this.goods = goods;

调用对象构造函数是不会产生新对象的,和调用普通函数一样,调用对象构造函数一般都是给对象属性赋值。
只有用运算符new,才会产生新对象,它会先创建一个空对象并将引用赋给this,返回this,然后再调用对象构造函数对对象this进行赋值。由此可见new才会产生新对象,而调用对象构造函数是不会产生新对象的。

所以call一番操作后,喷水车原型中的属性,就根本没有给它们赋值过,因此它们都是未定义的undefined

第三步将新建的继承了汽车原型的对象变成喷水车原型:

.prototype = car;//将实例car变成SprayCarModel的原型

注意:另忘了,喷水车原型依然是一个货车对象实例。其实,还可以通过创建一个对象字面量,然后用作原型,如
var d = {
start:function(){},

};
SprayCarModel.prototype = d;//SprayCarModel就将继承d中的属性和方法。

第四步向喷水车原型中添加属性和方法:

SprayCarModel.prototype.volume = 11;//设置原型的属性
//设置原型的方法
SprayCarModel.prototype.sprayWater = function(){
alert("spray spray spray");
};

第五步,测试:

//创建一个喷水车对象实例
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"));//喷水车对象实例的属性

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

下面给出完整的代码:

<!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>

原型链中的继承原理

对象调用方法或访问属性时,首先在对象实例里查找,如果查找不到,就会沿继承链上移,在其原型中接着查找。

意外!意外!意外!

console.log("SprayCarModel constructor is:"+sprayCar.constructor);

输出的结果是:

SprayCarModel constructor is:function CarModel(weight,height,goods){
this.weight = weight;
this.height = height;
this.goods = goods;
}
```

不可能呀,SprayCarModel才是sprayCar对象实例的构造器呀,怎么成了CarModel。原来,我们要显式地把对象构造函数的constructor属性设置为SprayCarModel对象构造器函数。虽然不设置也不会有什么影响,但是最佳实践建议还是设置的好

显式设置对象构造函数的constructor属性设置为SprayCarModel

SprayCarModel.prototype.constructor = SprayCarModel;

再看看结果:

console.log("SprayCarModel constructor is:"+sprayCar.constructor);

输出的结果是:

SprayCarModel constructor is:function SprayCarModel(name,handler){
this.name = name;
this.handler = handler;
}

这下终于正确了。

总结:

我们创建的每个原型链的终点都是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. constructor表示与原型相关联的构造函数
  2. hasOwnProperty判断实例是否有此属性,每个对象都有此方法。如果属性不是在对象实例中定义的,但能够访问它,就可以认为它肯定是在原型中定义的。
  3. isPrototypeOf判断一个对象是否是另一个对象的原型
    如car.isPrototypeOf(truck) //true
  4. 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>
~

扩展内置对象

其实与上面的一样,通过在原型中添加方法和属性,如扩展String内置对象:

String.prototype.clickme = function(){
alert("you clicked me");
};

其他的依次类推。

最后回顾一下:

  1. JavaScript对象系统使用原型式继承
  2. 使用构造函数创建对象实例时,实例包含自己的自定义属性,还有构造函数中方法的副本。
  3. 给构造函数的原型添加属性后,使用这个构造函数创建的实例都将继承这些属性。
  4. 通过在原型是中定义属性,可减少对象包含的重复代码。
  5. 要重写原型中的属性,只需在实例中添加该属性即可。
  6. 构造函数有默认的原型,可通过函数的属性prototype来访问它。
  7. 可将你自己创建的对象赋给构造函数的属性prototype
  8. 使用自定义的原型对象时,务必将原型的属性constructor设置为相应的对象构造函数,以保持一致。
  9. 给原型添加属性后,继承该原型的所有实例都将立即继承这些属性,即便是以前创建的实例也不例外。
  10. 归根结底,所有原型和对象都是从Object派生而来的。
  11. Object包含所有对象都将继承的属性和方法,如toString和hasOwnProperty
  12. 可給内置对象(如Object和String等)添加属性,也可重写它们的既有属性,但要小心。
  13. 在JavaScript中,一切几乎皆是对象,包括函数、数组和众多的内置对象和自己创建的自定义对象。

谢谢阅读。


举报

相关推荐

0 条评论