读书人

JavaScript prototype学习札记(转)

发布时间: 2012-10-26 10:30:59 作者: rapoo

JavaScript prototype学习笔记(转)
prototype源自法语,软件界的标准翻译为“原型”,代表事物的初始形态,也含有模型和样板的意义。学习了Java的面向对象思想,关于prototype的一些语法就很容易理解了,如属性、方法、继承、多态
JavaScript的所有function类型的对象都有一个prototype属性。这个prototype属性本身又是一个object类型的对象,因此我们也可以给这个prototype对象添加任意的属性和方法。既然prototype是对象的“原型”,那么由该函数构造出来的对象应该都会具有这个“原型”的特性。事实上,在构造函数的prototype上定义的所有属性和方法,都是可以通过其构造的对象直接访问和调用的。也可以这么说,prototype提供了一群同类对象共享属性和方法的机制。


//———————————————————
// 理解原型、构造、继承的示例
//———————————————————
function MyObject() {
this.v1 = ‘abc’;
}

function MyObject2() {
this.v2 = ‘def’;
}
MyObject2.prototype = new MyObject();

var obj1 = new MyObject();
var obj2 = new MyObject2();

alert(obj1.v1 + ” ” + obj2.v2 + ” ” + obj2.v1);

由alert可知道MyObject2具有了MyObject对象的属性

1). new()关键字的形式化代码
我们先来看“obj1 = new MyObject()”这行代码中的这个new关键字。
new关键字用于产生一个实例,但这个实例应当是从一个“原型的模板”复制过来的。这个用来作模板的原型对象,就是用“构造器
函数的prototype属性”所指向的那个对象。对于JavaScript“内置对象的构造器”来说,它指向内部的一个原型。

每一个函数,无论它是否用作构造器,都会有一个独一无二的原型对象。缺省时JavaScript用它构造出一个“空的初始对象实例(不是null)”。然而如果你给函数的这个prototype赋一个新的对象,那么构造过程将用这个新对象作为“模板”。

为了清楚地解释这个过程,我用代码形式化地描述一下这个过程:
//———————————————————
// new()关键字的形式化代码
//———————————————————
function new(aFunction) { // 如果有参数args
var _this = aFunction.prototype.clone(); // 从prototype中复制一个对象
aFunction.call(_this); // 调用构造函数完成初始化, (如果有,)传入args
return _this; // 返回对象
}

所以我们看到以下两点:
- 构造函数(aFunction)本身只是对传入的this实例做“初始化”处理,而不是构造一个对象实例。

- 构造的过程实际发生在new()关键字/运算符的内部。而且,构造函数(aFunction)本身并不需要操作prototype,也不需要回传this。

2). 由用户代码维护的原型(prototype)链
接下来我们更深入的讨论原型链与构造过程的问题。这就是:
- 原型链是用户代码创建的,new()关键字并不协助维护原型链

//———————————————————
// JS中“原型链表”的关键代码
//———————————————————
// 1. 构造器
function Animal() {};
function Mammal() {};
function Canine() {};
function Dog() {};

// 2. 原型链表
Mammal.prototype = new Animal();
Canine.prototype = new Mammal();
Dog.prototype = new Canine();

// 3. 示例函数
function isAnimal(obj) {
return obj instanceof Animal;
}

var dog = new Dog();
document.writeln(isAnimal(dog));

可以看到,在JS的用户代码中,“原型链表”的构建方法是一行代码:
“当前类的构造器函数”.prototype = “直接父类的实例”

3). 原型实例是如何被构造过程使用的
父类的构造过程仅仅发生在为原型(prototype属性)赋值的那一行代码上。其后,无论有多少个new MyObject2()发生,
MyObject()这个构造器都不会被使用。——这也意味着:
- 构造过程中,原型模板是一次性生成的;对这个原型实例的使用是不断复 制,而并不再调用原型的构造器。

我们先来看看下面的代码:

function Person(name)
{
this.name = name; //设置对象属性,每个对象各自一份属 性数据
};

Person.prototype.SayHello = function() //给Person函数的prototype添加SayHello方法。
{
alert(“Hello, I’m ” + this.name);
}

var BillGates = new Person(“Bill Gates”); //创建BillGates对象
var SteveJobs = new Person(“Steve Jobs”); //创建SteveJobs对象

BillGates.SayHello(); //通过BillGates对象直接调用到SayHello方法
SteveJobs.SayHello(); //通过SteveJobs对象直接调用到SayHello方法

alert(BillGates.SayHello == SteveJobs.SayHello); //因为两个对象是共享prototype的SayHello,所以显示:true

程序运行的结果表明,构造函数的prototype上定义的方法确实可以通过对象直接调用到,而且代码是共享的。显然,把方法设置到prototype的 写法显得优雅多了,尽管调用形式没有变,但逻辑上却体现了方法与类的关系,相对前面的写法,更容易理解和组织代码。

在JavaScript中,prototype不但能让对象共享自己财富,而且prototype还有寻根问祖的天性,从而使得先辈们的遗产可以代代相 传。当从一个对象那里读取属性或调用方法时,如果该对象自身不存在这样的属性或方法,就会去自己关联的prototype对象那里寻找;如果 prototype没有,又会去prototype自己关联的前辈prototype那里寻找,直到找到或追溯过程结束为止。

在JavaScript内部,对象的属性和方法追溯机制是通过所谓的prototype链来实现的。当用new操作符构造对象时,也会同时将构造函数的 prototype对象指派给新创建的对象,成为该对象内置的原型对象。对象内置的原型对象应该是对外不可见的,尽管有些浏览器(如Firefox)可以 让我们访问这个内置原型对象,但并不建议这样做。内置的原型对象本身也是对象,也有自己关联的原型对象,这样就形成了所谓的原型链。

在原型链的最末端,就是Object构造函数prototype属性指向的那一个原型对象。这个原型对象是所有对象的最老祖先,这个老祖宗实现了诸如 toString等所有对象天生就该具有的方法。其他内置构造函数,如Function, Boolean, String, Date和RegExp等的prototype都是从这个老祖宗传承下来的,但他们各自又定义了自身的属性和方法,从而他们的子孙就表现出各自宗族的那些 特征。

这就是JavaScript特有的“原型继承”。

JavaScript中的继承
function Person(name){
this.name = name; //设置对象属性,每个对象各自一份属性数据
};
Person.prototype.SayHello = function() //给Person函数的prototype添加SayHello方法。
{
alert(“Hello, I’m ” + this.name);
}
var javachen = new Person(“javachen”); //创建javachen对象
var website = new Person(“website”); //创建website对象
javachen.SayHello(); //通过javachen对象直接调用到SayHello方法
website.SayHello(); //通过website对象直接调用到SayHello方法
alert(javachen.SayHello == website.SayHello); //因为两个对象是共享prototype的SayHello,所以显示:true

对象可以掩盖原型对象的那些属性和方法,一个构造函数原型对象也可以掩盖上层构造函数原型对象既有的属性和方法。这种掩盖其实只是在对象自己身上创建了新的属性和方法,只不过这些属性和方法与原型对象的那些同名而已。JavaScript就是用这简单的掩盖机制实现了对象的“多态”性,与静态对象语言的虚函数和重载(override)概念不谋而合。

JavaScript有多种方式模拟继承.
1. 利用function:

function superClass() {
this.bye = superBye;
this.hello = superHello;
}

function subClass() {
this.inheritFrom = superClass;
this.inheritFrom();
this.bye = subBye;
}

或者:

function subClass() {
superClass.call(this);
}

先定义subClass的inheritFrom方法, 再调用这个方法(方法名称并不重要), 或者直接使用FunctionObject 的call 方法将this做参数, 都可以模拟实现从superClass的继承. 注意调用superClass时的this指向. 这个方法就是在执行subClass的cunstructor function时, 先执行supperClass的cunstructor function.这个方法的缺点在于子类仅仅是在自己的构造函数中, 将this作为参数调用了父类的构造函数, 将构造函数赋予父类的所有域赋予子类. 所以, 任何父类在构造函数之外(通过prototype)定义的域, 子类都无法继承. 而且子类的构造函数一定要在定义自己的域之前调用父类的构造函数, 免得子类的定义被父类覆盖. 使用这种方法子类也尽量不要使用prototype来定义子类的域, 因为prototype的定义在子类new的之后就执行, 所以它一定会在调用父类构造函数前, 同样会有被父类的定义覆盖的危险.

2. 利用prototype:

function superClass() {
this.bye = superBye;
this.hello = superHello;
}

function subClass() {
this.bye = subBye;
}
subClass.prototype = new superClass();
subClass.prototype.constructor = superClass;

这里将一个superClass的实例设置成subclass的原型:protytype, 由于new superClass实例一定会调用父类prototype定义的所有域, 所以这种方法避免了上一种方法的一个问题, 父类可以通过prototype来描述域. 可以实现从superClass的继承. 而这个方法也有缺点, 由于子类的peototype已经是父类的实例(Object实例), 不能再被实例化, 所以new子类实例的时候, 父类的所有非基本数据类型(见JavaScript数据类型)都将是reference copy而非数据copy. 简单说就是所有的父类域在子类中虽然存在, 但看起来就像Java中的static域一样在子类间share.被一个子类改变, 所有子类都会改变.

注意这里的最后一句, 改变了子类prototype中的constructor属性. 它对子类使用没有影响, 仅仅是为了在调用instanceOf方法时它使得子类实例返回subClass.

3. Parasitic Inheritance (寄生继承)
function superClass() {
this.bye = superBye;
this.hello = superHello;
}

function subClass() {
this.base = new supperClass();
base.sayBye = subBye;
return base;
}

这种继承其实是一种扩展, 因为在调用instanceOf时, 子类会返回父类名称, 它的好处在于在构造函数继承的基础上解放了父类, 父类可以使用prototype定义自己的域, 但是子类仍然不建议使用prototype,以免被父类覆盖. 为了可以使子类的instanceof返回正确类型, 我们可以再改进一下:

function subClass() {
this.base = new supperClass();
for ( var key in this.base ) {
if ( !this[key] ) {
this[key] = this.base[key];
}
}

this.sayBye = subBye;
}

将所有的父类域拷贝给子类一份, 不再返回父类, instanceof子类实例时就可以返回正确类型。

读书人网 >JavaScript

热点推荐