JS原型+构造函数创建对象

JS原型+构造函数创建对象
 

JavaScript对象的创建方式

在JavaScript中,创建对象的方式包括两种:对象字面量和使用new表达式。对象字面量是一种灵活方便的书写方式,例如:

?
1
2
3
4
5
6
var o1 = {
     p:”I’m in Object literal”,
     alertP:function(){
         alert( this .p);
     }
}

这样,就用对象字面量创建了一个对象o1,它具有一个成员变量p以及一个成员方法alertP。这种写法不需要定义构造函数,因此不在本文的讨论范围之内。这种写法的缺点是,每创建一个新的对象都需要写出完整的定义语句,不便于创建大量相同类型的对象,不利于使用继承等高级特性。

new表达式是配合构造函数使用的,例如new String(“a string”),调用内置的String函数构造了一个字符串对象。下面我们用构造函数的方式来重新创建一个实现同样功能的对象,首先是定义构造函数,然后是调用new表达式:

?
1
2
3
4
5
6
7
function CO(){
     this .p = “I’m in constructed object”;
     this .alertP = function(){
         alert( this .p);
     }
}
var o2 = newCO();

那么,在使用new操作符来调用一个构造函数的时候,发生了什么呢?其实很简单,就发生了四件事:

?
1
2
3
4
var obj  ={};
obj.__proto__ = CO.prototype;
CO.call(obj);
return   obj;

第一行,创建一个空对象obj。

第二行,将这个空对象的__proto__成员指向了构造函数对象的prototype成员对象,这是最关键的一步,具体细节将在下文描述。

第三行,将构造函数的作用域赋给新对象,因此CA函数中的this指向新对象obj,然后再调用CO函数。于是我们就给obj对象赋值了一个成员变量p,这个成员变量的值是” I’min constructed object”。

第四行,返回新对象obj。当构造函数里包含返回语句时情况比较特殊,这种情况会在下文中说到。


正确定义JavaScript构造函数

不同于其它的主流编程语言,JavaScript的构造函数并不是作为类的一个特定方法存在的;当任意一个普通函数用于创建一类对象时,它就被称作构造函数,或构造器。一个函数要作为一个真正意义上的构造函数,需要满足下列条件:

1、 在函数内部对新对象(this)的属性进行设置,通常是添加属性和方法。

2、 构造函数可以包含返回语句(不推荐),但返回值必须是this,或者其它非对象类型的值。

上文定义的构造函数CO就是一个标准的、简单的构造函数。下面例子定义的函数C1返回了一个对象,我们可以使用new表达式来调用它,该表达式可以正确返回一个对象:

?
1
2
3
4
5
6
7
8
function C1(){
     var o = {
         p:”I’m p in C1”
     }
     return   o;
}
var o1 =  new   C1();
alert(o1.p); //I’m p in C1

但这种方式并不是值得推荐的方式,因为对象o1的原型是函数C1内部定义的对象o的原型,也就是Object.prototype。这种方式相当于执行了正常new表达式的前三步,而在第四步的时候返回了C1函数的返回值。该方式同样不便于创建大量相同类型的对象,不利于使用继承等高级特性,并且容易造成混乱,应该摒弃。

一个构造函数在某些情况下完全可以作为普通的功能函数来使用,这是JavaScript灵活性的一个体现。下例定义的C2就是一个“多用途”函数:

?
1
2
3
4
5
6
7
8
9
10
function C2(a, b){
     this .p = a + b;
     this .alertP = function(){
         alert( this .p);
     }
     return   this .p; //此返回语句在C2作为构造函数时没有意义
}
var c2 =  new   C2( 2 , 3 );
c2.alertP(); //结果为5
alert(C2( 2 3 ));  //结果为5

该函数既可以用作构造函数来构造一个对象,也可以作为普通的函数来使用。用作普通函数时,它接收两个参数,并返回两者的相加的结果。为了代码的可读性和可维护性,建议作为构造函数的函数不要掺杂除构造作用以外的代码;同样的,一般的功能函数也不要用作构造对象。


为什么要使用构造函数

根据上文的定义,在表面上看来,构造函数似乎只是对一个新创建的对象进行初始化,增加一些成员变量和方法;然而构造函数的作用远不止这些。为了说明使用构造函数的意义,我们先来回顾一下前文提到的例子。执行var o2 = new CO();创建对象的时候,发生了四件事情:

?
1
2
3
4
var obj  ={};
obj.__proto__ = CO.prototype;
CO.call(obj);
return   obj;

我们说最重要的是第二步,将新生成的对象的__prop__属性赋值为构造函数的prototype属性,使得通过构造函数创建的所有对象可以共享相同的原型。这意味着同一个构造函数创建的所有对象都继承自一个相同的对象,因此它们都是同一个类的对象。关于原型(prototype)和继承的细节,笔者会再另一篇文章中深入说明。

在JavaScript标准中,并没有__prop__这个属性,不过它现在已经是一些主流的JavaScript执行环境默认的一个标准属性,用于指向构造函数的原型。该属性是默认不可见的,而且在各执行环境中实现的细节不尽相同,例如IE浏览器中不存在该属性。我们只要知道JavaScript对象内部存在指向构造函数原型的指针就可以了,这个指针是在调用new表达式的时候自动赋值的,并且我们不应该去修改它。

在构造对象的四个步骤中,我们可以看到,除第二步以外,别的步骤我们无须借助new表达式去实现,因此new表达式不仅仅是对这四个步骤的简化,也是要实现继承的必经之路。


容易混淆的地方

关于JavaScript的构造函数,有一个容易混淆的地方,那就是原型的constructor属性。在JavaScript中,每一个函数都有默认的原型对象属性prototype,该对象默认包含了两个成员属性:constructor和__proto__。关于原型的细节就不在本文赘述了,我们现在关心的是这个constructor属性。

按照面向对象的习惯性思维,我们说构造函数相当于“类”的定义,从而可能会认为constructor属性就是该类实际意义上的构造函数,在new表达式创建一个对象的时候,会直接调用constructor来初始化对象,那就大错特错了。new表达式执行的实际过程已经在上文中介绍过了(四个步骤),其中用于初始化对象的是第三步,调用的初始化函数正是“类函数”本身,而不是constructor。如果没有考虑过这个问题,这一点可能不太好理解,那就让我们举个例子来说明一下吧:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
function C3(a, b){
     this .p = a + b;
     this .alertP = function(){
         alert( this .p);
     }
}
//我们定义一个函数来覆盖C3原型中的constructor,试图改变属性p的值
function fake(){
     this .p =  100 ;
}
C3.prototype.constructor = fake;  //覆盖C3原型中的constructor
var c3 =  new   C3( 2 , 3 );
c3.alertP(); //结果仍然为5

上述代码手动改变了C3原型中的constructor函数,然而却没有对c3对象的创建产生实质的影响,可见在new表达式中,起初始化对象作用的只能是构造函数本身。那么constructor属性的作用是什么呢?一般来说,我们可以使用constructor属性来测试对象的类型:

?
1
2
var myArray = [ 1 , 2 , 3 ];
(myArray.constructor == Array);  // true

这招对于简单的对象是管用的,涉及到继承或者跨窗口等复杂情况时,可能就没那么灵光了:

?
1
2
3
4
5
6
7
function f() {  this .foo =  1 ;}
function s() {  this .bar =  2 ; }
s.prototype =  new   f();  // s继承自f
   
var son =  new   s();  // 用构造函数s创建一个子类对象
(son.constructor == s);  // false
(son.constructor == f);  // true

这样的结果可能跟你的预期不相一致,所以使用constructor属性的时候一定要小心,或者干脆不要用它。



使用JS对象的原型有利于数据的共享,但这个也是它的缺点,若创建对象的时候采用了原型中定义的数据,那么若一旦某个对象修该了数据就会影响到其他的对象,也就是说原型在保持数据共享的基础上也破坏了数据的独立性。所以在用JS创建对象的时候,最好是采用组合构造函数+原型模式
下面举一例子:
function Box(name,age){
 this.name=name;
 this.age=age;
 this.family=['父亲','母亲','妹妹'];
}
Box.prototype={
 constructor:Box, //让原型指向Box对象
 run:function(){
  return this.name+this.age;
 }
}

var box1 = new Box('xiaofeng',18);
box1.family.push('弟弟');
alert(box1.family);
结果为:父亲,母亲,妹妹,弟弟
alert(box1.run);
结果为:xiaofeng18

var box2 = new Box('bin',20);
alert(box2.famuly);
结果为:父亲,母亲,妹妹 //保持了数据的独立性
alert(box2.run);
结果为:bin20   //数据共享

PS:上面采用的这种组合构造函数+原型模式创建对象确实比较好的解决了对象数据的共享与独立,不过,这种方式看上去不太符合面向对象的类型,因此,可以采用动态的组合构造函数+原型模式
如下:
function Box(name,age){
 this.name=name;
 this.age=age;
 this.family=['父亲','母亲','妹妹'];
 //判断是不是已经存在,若不存在泽创建(若不经过这一步判断,则每次创建对象的时候都会创建一次run方法)
 if(typeof this.run != 'function'){ 
  Box.prototype.run=function(){
   return this.name+this.age;
  }
 }
}

 

JavaScript中定义"类"的时候可以通过构造函数和原型的方式来实现,它们之间有何区别和利弊呢?

首先来看一个通过构造函数实现类定义的例子:

[javascript]   view plain copy
  1. function Car(color,model,drivers){  
  2.     this.color=color;  
  3.     this.model=model;  
  4.     this.drivers=drivers;  
  5.       
  6.     this.blar = function(){  
  7.         alert("I am a car: " + model + " in " + color + ". " + drivers + " can drive me" );  
  8.     }  
  9. }  
  10.   
  11. var car1 = new Car('red','BMW',['Mike','Kevin']);  
  12. car1.blar();  
  13.   
  14. var car2 = new Car('red','BMW',['Mike','Kevin']);  
  15. car2.blar();  

在上面的例子中,每一个实例中,函数blar都会拷贝到实例中,弊端就是浪费内存。
在来看一个通过利用prototype来定义类的例子:

[javascript]   view plain copy
  1. function Drivers(){  
  2.     var names="";  
  3.     this.addDriver= function(name){  
  4.         names = names +" " + name;  
  5.     }  
  6.     this.toString = function(){  
  7.         return  names;  
  8.     }  
  9. }  
  10.   
  11.   
  12. function Car(){  
  13. }  
  14.   
  15. Car.prototype.color = "red";  
  16. Car.prototype.model = "BMW";  
  17.   
  18. //Car.prototype.drivers = ['Mike','Kevin'];  
  19.   
  20. Car.prototype.drivers =new Drivers();  
  21.   
  22. Car.prototype.blar = function(){  
  23.         alert("I am a car: " + this.model + " in " + this.color + ". " + this.drivers + " can drive me" );  
  24.     };  
  25.   
  26. var car1 = new Car();  
  27. car1.color='Blue';  
  28. //car1.drivers.push('Luios');  
  29. car1.drivers.addDriver('Mike');  
  30. car1.drivers.addDriver('Kevin');  
  31. car1.drivers.addDriver('Luios');  
  32.   
  33. car1.blar();//outputs I am a car: BMW in Blue. Mike,Kevin,Luios can drive me  
  34. var car2 = new Car();  
  35. car2.blar(); //outputs I am a car: BMW in red. Mike,Kevin,Luios can drive me  

prototype方式定义的方式,函数不会拷贝到每一个实例中,所有的实例共享prototype中的定义,节省了内存。但是属性如果是对象的话,所有实例也是共享同一个对象,
如上例中的drivers使用自定义对象或者数组的时候,如果其中某一个实例改变了其中的值,所有的实例的值都被改变。因为所有实例的属性指向的是同一个对象的引用,如果上面的例子中car2.drivers=['Ivy','Lili'];来改变实例car2的属性内容,相对于car2.drivers指向了另一个对象(这时候car2有一个实例属性,有一个prototype属性都是drivers)。
Javascript是一种动态语言,实例创建之后可以动态添加属性和方法,在“构造函数”的定义之外也可以添加属性和方法。其实,下面例子中sayHi本身也是一个全局的类型为Function的实例。

[javascript]   view plain copy
  1. var obj = {};  
  2. obj.prop="value";  
  3. obj.test = function(){  
  4.     alert("test function: " + this.prop);  
  5. }  
  6. obj.test();  
  7.   
  8. function sayHi() {  
  9.     alert("hi");  
  10. }  
  11.   
  12. sayHi.sayHello = function() {  
  13.     alert("hello");  
  14. };  
  15. sayHi.sayHello();//outputs hello  

注意如果想让添加的属性或者方法能够被实例使用,要使用prototype添加,下面的例子说明了这一点

[javascript]   view plain copy
  1. function sayHi() {  
  2.     alert("hi");  
  3. }  
  4.   
  5. sayHi.sayHello = function() {  
  6.     alert("hello");  
  7. };  
  8.   
  9. sayHi.sayHello();//outputs hello  
  10.   
  11. var osayHi = new sayHi();//outputs hi  
  12. osayHi.sayHello(); // TypeError: osayHi.sayHello is not a function  

因为不是通过在prototype中以这样的方式添加的(sayHi.prototype.sayHello),实例是不能访问sayHello方法的,只能通过sayHi.sayHello(类似如静态方法)的方式访问。同样如果添加的时候是通过prototype方式,则不能用静态方式访问。

[javascript]   view plain copy
  1. function sayHi() {  
  2.     alert("hi");  
  3. }  
  4.   
  5. sayHi.prototype.sayHello = function() {  
  6.     alert("hello");  
  7. };  
  8.   
  9. var osayHi = new sayHi();// outputs hi  
  10. osayHi.sayHello(); // outputs hello  
  11.   
  12. sayHi.sayHello(); //TypeError: sayHi.sayHello is not a function  


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值