?
?? ? ?JavaScript是一种面向对象的脚本语言,但是JavaScript中的对象与其他语言(尤其是像Java、C#这样的静态语言)有很大的不同,JavaScript中的对象是基于原型的。原型是对象的基础,它定义并实现了一个新对象所必须包含的成员列表,并被所有同类对象实例所共享。与其他语言中类的概念相比,原型更像是类的静态成员。本文就JavaScript中对象的创建、继承做初步的讲解,并探讨下对象成员相关的一些概念和特性。(斑头雁原创:http://bantouyan.iteye.com)
一、创建对象
?? ? ?在JavaScript使用newcaozuofu.html" target="_blank">操作符中创建对象,或者用直接量创建对象。与其他语言不同,JavaScript对象在完成创建后仍能增加成员,比如如下的代码:
var obj = new Object(); obj.name = 'alice'; obj.age = 18; obj.getSalary = function(){return 8000 - this.age;};
?这段代码首先创建了一个Object类的对象,然后增加了三个成员。除了用new操作符创建对象外,我们还可以用直接量表示一个对象,如下面的代码:
var obj = { name: 'alice', age: 18, getSalary: function(){return 8000 - this.age;} };
这段代码创建了一个对象,与前面代码创建的对象一样,但是相比较起来,用直接量方式更加简单明了。(斑头雁原创:http://bantouyan.iteye.com)
二、创建类
?? ? ?除了可以使用JavaScript内建的类外,我们还可以定义自己的类,定义类的方法一般有工厂方法、构造函数、原型方法和混合方法。
???
1、工厂方法
?? ? ?创建对象的传统方式是首先创建一个Object实例,然后增加成员属性,如果把这些代码用一个函数封装起来并返回所创建的对象,那这个函数就是创建对象的工厂函数。使用工厂函数的示例代码如下:
function pgetSalary() {return 8000 - this.age;} function Employee(name, age){ var obj = new Object(); obj.name = name; obj.age = age; obj.getSalary = pgetSalary; return obj; } var obja = Employee('alice', 18); var objb = new Employee('cindy', 20, true); alert(obja instanceof Employee); //output false
采用工厂方式可以方便的创建大批量类似对象,但是工厂函数的名字并不是类的名字,虽然可以用new操作符,但仍无法使用instanceof来判断所创建的对象的类型。另外,如果把成员函数的函数体定义在工厂函数的内部的话,创建对象时会重复的创建成员函数实例(在JavaScript中函数也是一种对象),浪费存储空间。(斑头雁原创:http://bantouyan.iteye.com)
?? ? ? ?
2、构造函数方法
?? ? ?构造函数类似工厂函数,所不同的是构造函数的名字就是类名,可以用new操作符来创建对象。
function pgetSalary() {return 8000 - this.age;} function Employee(name, age){ this.name = name; this.age = age; this.getSalary = pgetSalary; } var obja = new Employee('alice', 18); var objb = new Employee('cindy', 19); alert(obja instanceof Employee); //output true
同工厂方式一样,如果类有成员函数,最好定义在构造函数意外,否则会因成员函数的重复创建而浪费空间。用构造函数创建对象的好处是所生成的对象可以用instanceof来判断所属的类。(斑头雁原创:http://bantouyan.iteye.com)
?? ? ? ?
3、原型方法
?? ? ?原型是对象的模板,所以我们可以用prototype属性来定义一个类,通常的做法是定义一个空的构造函数,然后在prototype属性上定义类的成员,示例代码如下:
function Employee(){ } Employee.prototype.name = 'alice'; Employee.prototype.age = 18; Employee.prototype.getSalary = function() {return 8000 - this.age;}; Employee.prototype.languages = ['Java', 'C#']; var obja = new Employee(); var objb = new Employee(); alert(obja instanceof Employee); //output true alert(obja.languages); //output Java,C# alert(objb.languages); //output Java,C# objb.languages.push('perl'); var objc = new Employee(); alert(obja.languages); //output Java,C#,perl alert(objb.languages); //output Java,C#,perl alert(objc.languages); //output Java,C#,perl
原型方式同构造函数方式一样可以用instanceof判断所创建的对象的类型,但是,原型方式的构造函数不能带任何参数,除此之外,如果某个成员是一个对象实例,那么对这个成员的修改可能会影响到这个类创建的其他对象以及在此之后创建的对象。如示例代码中仅修改了对象objb的languages属性,而在此之前创建的obja与在此之后创建的objc的languages属性也受到了影响。(斑头雁原创:http://bantouyan.iteye.com)
?? ?
4、混合方式(构造函数+原型)
?? ? ?原型方式的构造函数不能带有参数,这样的类在使用中显得很不方便,因此,把原型方式与构造函数综合起来使用是一种更为实用的方法,示例代码如下:
function Employee(name, age){ this.name = name; this.age = age; this.languages = ['Java', 'C#']; } Employee.prototype.getSalary = function(){return 8000 - this.age;}; var obja = new Employee('alice', 18); var objb = new Employee('cindy', 20); alert(obja.languages); //output Java,C# alert(objb.languages); //output Java,C# objb.languages.push('perl'); var objc = new Employee(); alert(obja.languages); //output Java,C# alert(objb.languages); //output Java,C#,perl alert(objc.languages); //output Java,C#
混合方式利用构造函数定义非函数成员,用prototype定义函数成员,既避免了因重复创建成员函数造成的空间浪费,又不会造成对象实例间的相互影响(如示例代码中修改objb的languages成员不会影响到obja和objc),是一种比较好的定义类的方式。(斑头雁原创:http://bantouyan.iteye.com)
三、类的继承
?? ? ?JavaScript根本就没有继承类的机制,不过借助JavaScript的一些语言特性,我们仍然可以模拟这种行为,常用的方法有对象冒充、原型链和混合方式。(斑头雁原创:http://bantouyan.iteye.com)
1、对象冒充
?? ? ?JavaScript函数中this会根据函数的调用上下文的不同指向不同的对象,根据这个特性,开发者总结出了对象冒充方式的继承方法,示例代码如下:
function Employee(name, age) { this.name = name; this.age = age; this.languages = ['Java', 'C#']; } Employee.prototype.getSalary = function(){return 8000 - this.age;}; function Manager(name, age, level) { Employee.call(this, name, age); //Employee.apply(this, [name, age]); this.level = level; } var ma = new Manager('alice', 18, 1); alert(ma instanceof Employee); //output false alert(ma instanceof Manager); //output true alert(ma.name); //output alice alert('getSalary' in ma); //output false
Fucntion.call()与Fucntion.apply()会执行函数Function本身,只不这两个方法的第一个参数都是用来代替Function内部的this,这两个函数所不同的地方在于call把Function的参数顺序放在第一个参数之后,apply把Function的参数按顺序组成一个数组作为第二个参数调用。
?? ? ?在示例代码中,构造函数Manager调用了Employee的call方法,所以执行嵌套的Employee时内部的this指向的是Manager的this,这样用Manager创建的对象就有了Employee的成员。
?? ? ?对象冒充方式继承有一个好处就是可以实现多继承,像下面的代码就可以实现多继承。
function ClassC(){ ClassA.call(this); ClassB.call(this); }
如果ClassA与ClassB有相同名字的成员,那么会因为调用顺序导致ClassB的成员定义会覆盖ClassA的成员定义,这在使用时要加以注意。
?? ? ?对象冒充继承方式的问题在于它无法使用instanceof来判断对象是不是超类的实例,另外也无法继承定义在超类prototype属性上的成员(如示例代码中的成员函数getSalary么有被类Manager继承)。(斑头雁原创:http://bantouyan.iteye.com)
2、原型链
?? ? ?原型链继承与用原型方式创建类一样,方法是构造一个空构造函数,然后用超类的实例代替子类的prototype,示例代码如下:
function Employee(name, age) { this.name = name; this.age = age; this.languages = ['Java', 'C#']; } Employee.prototype.getSalary = function(){return 8000 - this.age;}; function Manager(){ } Manager.prototype = new Employee(); var ma = new Manager(); alert(ma instanceof Employee); //output true alert(ma instanceof Manager); //output true alert('name' in ma); //output true alert('getSalary' in ma); //output true
原型链方式的优点是可以继承超类的所有成员,包括定义在超类prototype上的成员,而且可以用instanceof检测是不是超类的实例,缺点是构造函数不能带参数。(斑头雁原创:http://bantouyan.iteye.com)
3、混合方式(对象冒充+原型链)
?? ? ?如同创建类时可以把构造函数与原型综合在一起一样,继承时也可以把对象冒充方法与原型链综合起来使用,示例代码如下:
function Employee(name, age) { this.name = name; this.age = age; this.languages = ['Java', 'C#']; } Employee.prototype.getSalary = function(){return 8000 - this.age;}; function Manager(name, age, level) { Employee.call(this, name, age); //Employee.apply(this, [name, age]); this.level = level; } Manager.prototype = new Employee(); var ma = new Manager(); alert(ma instanceof Employee); //output true alert(ma instanceof Manager); //output true alert('name' in ma); //output true alert('getSalary' in ma); //output true
混合方式既可以继承超类所有的成员,又可以使用带参数的构造函数,还可以用instanceof检测对象是不是超类的实例,可以说既综合了对象冒充与原型链的好处,又回避了他们的缺点,是一种比较实用的继承方法。?? ? ?
四、类的原型
?? ? ?在JavaScript中不光可以随时给对象添加成员,而且可以随时修改已有的类的定义,并能影响到该类所有的实例(包括已经创建的和未创建的),这要归功于JavaScript的原型机制。原型规定了类所必须具有的成员列表,而且,原型上的这些成员与成员的值还会被该类所有的实例(包括子类的实例)共享。(斑头雁原创:http://bantouyan.iteye.com)
?? ? ?JavaScript对象的成员分为两种,即实例成员和原型成员。实例成员一般是在构造函数中通过给this变量添加的成员,或给已创建好的对象添加的成员,一个对象的实例成员与其他对象无关,即使是同一个类的实例,也可能有不同的实例成员。原型成员都是通过类的prototype属性定义的,它的名字与值被类的所有实例共享,看下面的代码:
function ClassA(){ this.im = "instance_member"; } ClassA.prototype.pm = [1, 2, 3]; var obja = new ClassA(); alert(obja.pm); //output 1,2,3 var objb = new ClassA(); alert(objb.pm); //output 1,2,3 ClassA.prototype.foo = function() {alsert(this.im);}; objb.pm.push(4); var objc = new ClassA(); alert(obja.pm); //output 1,2,3,4 alert(objb.pm); //output 1,2,3,4 alert(objc.pm); //output 1,2,3,4 obja.foo(); //output instance_member objb.foo(); //output instance_member objc.foo(); //output instance_member
我们仅仅修改了对象objb的原型成员pm,然而在此之前创建的obja与在此之后创建的objc的成员都受到了影响,不光如此,我们还可以随时修改类,如代码中创建objb后我们给ClassA的原型添加了一个新的成员foo,在此之前创建的obja、objb和再次之后创建的objc就都具有了新成员函数foo。
?? ? ?在获取对象的成员时,JavaScript首先查找对象的实例成员中有没有同名的成员,如果没有则查找原型成员,如果在原型中仍找不到的话就继续查找原型的原型,直到找到或者搜索完整个原型链为止。对成员进行赋值操作时,如果对象已经有一个同名的实例成员,则修改这个实例成员的值,否则将创建一个新的实例成员(即使存在同名的原型成员)。所以如果有一个实例成员的名字与某个原型成员的名字相同,那么这个原型成员将被屏蔽,看下面的代码:
function ClassA(){ this.im = "instance_member"; } ClassA.prototype.pm = [1, 2, 3]; var obja = new ClassA(); obja.pm = 'alice'; var objb = new ClassA(); alert(obja.pm); //output alice alert(objb.pm); //output 1,2,3,4
我们创建对象obja后给obja.pm进行赋值操作,尽管ClassA存在原型成员pm,JavaScript仍然给obja创建了一个同名的实例成员,在获取成员pm时,由于首先查找obja的实例成员,所以同名的原型成员被屏蔽,而objb没有这样的实例成员,所以得到的仍是原型成员的值。
?? ? ?我们可以用for-in循环遍历对象所有的成员,如果要检测对象是否具有某个成员,就要依赖JavaScript提供两种方法,一种是in操作符,另外一种是hasOwnProperty()方法。这两种方法的区别是如果hasOwnProperty()返回false,则肯定不是对象的实例成员,如果in操作符返回false,则肯定不是对象的成员。结合前面提到的继承方法,我们看一下对象成员的继承关系,示例代码如下:(斑头雁原创:http://bantouyan.iteye.com)
function ClassA(){ this.am = 'am'; } ClassA.prototype.ap = 'ap'; function ClassB1(){ ClassA.call(this); this.bm = 'bm'; } ClassB1.prototype.bp = 'bp'; function ClassB2(){ this.bm = 'bm'; } ClassB2.prototype = new ClassA(); ClassB1.prototype.bp = 'bp'; function ClassB3(){ ClassA.call(this); this.bm = 'bm'; } ClassB3.prototype = new ClassA(); ClassB1.prototype.bp = 'bp'; var b1 = new ClassB1(); alert(b1.hasOwnProperty('am')); //output true alert('am' in b1); //output true alert(b1.hasOwnProperty('ap')); //output false alert('ap' in b1); //output false var b2 = new ClassB2(); alert(b2.hasOwnProperty('am')); //output false alert('am' in b2); //output true alert(b2.hasOwnProperty('ap')); //output false alert('ap' in b2); //output true var b3 = new ClassB3(); alert(b3.hasOwnProperty('am')); //output true alert('am' in b3); //output true alert(b3.hasOwnProperty('ap')); //output false alert('ap' in b3); //output true
从示例代码的运行结果我们可以看到,采用对象冒充方式继承,超类的实例成员会被继承为子类的实例成员,超类的原型成员则不会被继承,因此'ap' in b1将返回false;采用原型链方式继承,超类的实例成员与原型成员都会被继承为子类的原型成员;采用混合方式继承,超类的实例成员会被继承为子类的实例成员,超类的原型成员会被继承为子类的原型成员。
?? ? ?实际上类的prototype也是一个对象,我们用for-in循环来遍历下这些类的prototype属性,代码如下:(斑头雁原创:http://bantouyan.iteye.com)
function ClassA(){ this.am = 'am'; } ClassA.prototype.ap = 'ap'; function ClassB1(){ ClassA.call(this); this.bm = 'bm'; } ClassB1.prototype.bp = 'bp'; function ClassB2(){ this.bm = 'bm'; } ClassB2.prototype = new ClassA(); ClassB2.prototype.bp = 'bp'; function ClassB3(){ ClassA.call(this); this.bm = 'bm'; } ClassB3.prototype = new ClassA(); ClassB3.prototype.bp = 'bp'; var str = "ClassB1\n"; var prot = ClassB1.prototype; for(var p in prot) { str += p + ': ' + prot.hasOwnProperty(p) + ' ' + (p in prot) + "\n"; } alert(str); //output ClassB1 //output bp: true true var str = "ClassB2\n"; var prot = ClassB2.prototype; for(var p in prot) { str += p + ': ' + prot.hasOwnProperty(p) + ' ' + (p in prot) + "\n"; } alert(str); //output ClassB2 //output am: true true //output ap: false true //output bp: true true var str = "ClassB3\n"; var prot = ClassB3.prototype; for(var p in prot) { str += p + ': ' + prot.hasOwnProperty(p) + ' ' + (p in prot) + "\n"; } alert(str); //output ClassB3 //output am: true true //output ap: false true //output bp: true true
从这段代码的运行结果我们可以看出,采用对象冒充方式继承时,子类的原型没有任何从超类继承来的成员;采用原型链方式继承时,超类的实例成员继承为子类原型的实例成员,超类的原型成员继承为子类原型的原型成员;采用混合方式继承时,超类的实例成员继承为子类原型的实例成员,超类的原型成员继承为子类原型的原型成员。综合两段代码我们可以看出,在采用混合方式继承时,对于超类的每一个实例成员,子类都有一个对应的实例成员和一个对应的原型成员,不过原型成员会被实例成员屏蔽。(斑头雁原创:http://bantouyan.iteye.com)
?? ? ?类的原型仍然具有原型成员,说明原型之上还有原型,这些相关联的原型就组成了原型链。前文说过,查找对象的成员时,会从实例成员开始,顺着原型链向上查找,如果一个成员在原型链上的位置越靠上,那么需要查找的操作就越多,花费的代价就越大。不过目前比较新的浏览器都做了这方面的优化,不用再关心这类问题。(斑头雁原创:http://bantouyan.iteye.com)