JavaScript 面向对象的先后设计

面向对象(Object-oriented,OO)的言语有一个标志,那正是它们都有类的定义。而透过类能够创建任意多少个拥有相同属性和办法的指标。前面提到过,ECMAScript中平素不类的定义,由此它的对象也与基于类的言语中的对象有所差别。

ECMAScript-262把目的定义为:“严节属性的聚合,其性质能够包括基本值、对象或许函数。”严刻来讲,那就一定于说对象是一组没有特定顺序的值。对象的每种属性或方式都有五个名字,而种种名字都映射到一个值。正因为如此(以及别的即将研究的由来),大家能够把ECMAScript的目的想象成散列表:无非就是一组名值对,当中值能够是数量或函数。

各种对象都以基于三个引用类型成立的,那个引用类型能够是前面议论的原生类型,也得以是开发人士定义的花色。

1,成立对象

开创自定义对象最简便的办法正是创建3个Object的实例,然后再为它添加属性和格局,如下所示:

var person = new Object();
person.name = 'Nicholas';
person.age = 29;
person.job = 'Software Engineer';

person.sayName = function() {
    alert(this.name);
};

person.sayName(); //"Nicholas"

 下面的事例创造了多个名为person的目的,并为它添加了多性格格(name、age和job)和四个措施(sayName())。个中,sayName()方法用于显示this.name(将被解析成person.name)的值。早期的JavaScript开发职员日常使用那几个形式来创立新指标。但那种方式有个让人惊讶标欠缺:使用同一个接口创制很多对象,会产生多量的再度代码。为缓解那几个标题,人们开头运用工厂情势的一种变体。

1.1 工厂情势

厂子形式是软件工程领域一种广为人知的设计情势,那种形式抽象了创立具体指标的历程(后边还讲钻探别的设计情势及其在JavaScript中的完成)。考虑到在ECMAScript中不可能成立类,开发职员就发明了一种函数,用函数来封装以特定接口创建对象的底细,如下边包车型客车例子所示:

function createPerson(name, age, job) {
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function() {
        alert(this.name);
    };
    return o;
}

var person1 = createPerson('Nicholas', 29, 'Software Engineer');
var person2 = createPerson('Greg', 27, 'Doctor');

person1.sayName(); //"Nicholas"
person2.sayName(); //"Greg"

 函数createPerson()能够基于接收的参数来创制三个饱含全数须求音讯的Person对象。能够多多次地调用那个函数,而每回它都会再次来到二个暗含多个天性三个格局的靶子。工厂情势就算缓解了创设四个一般对象的题材,但却尚未缓解对象识其余难题(即如何精通3个对象的品类)。随着JavaScript的发展,又二个新格局出现了。

1.2 构造函数情势

ECMAScript中的构造函数可用来成立特定类型的对象。像Object和Array那样的原生构造函数,在运转时会自动出现在履行环境中。别的,也足以创设自定义的构造函数,从而定义自定义对象类型的性质和艺术。例如,可以动用构造函数形式将日前的例证重写如下:

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function() {
        alert(this.name);
    };
}

var person1 = new Person('Nicholas', 29, 'Software Engineer');
var person2 = new Person('Greg', 27, 'Doctor');

person1.sayName(); //"Nicholas"
person2.sayName(); //"Greg"

 在这么些事例中,Person()函数取代了createPerson()函数。大家注意到,Person()中的代码出了与createPerson()中平等的一些外,还留存以下不相同之处:

a,没有显式地创制对象;

b,直接将质量和办法赋给了this对象;

c,没有return语句。

其余,还应有小心到函数名Person使用的是大写字母P。依照规矩,构造函数始终都应该以五个大写字母早先,而非构造函数应该以2个小写字母开始。那些做法借鉴自其余OO语言,重借使为了分裂于ECMAScript中的其余函数;因为构造函数本人也是函数,只但是能够用来创立对象而已。

要制造Person的新实例,必须利用new操作符。以那种办法调用构造函数实际上会经历以下多个步骤:

01,创立1个新指标;

02,将构造函数的效益域赋给新对象(由此this就对准了那些新指标);

03,执行构造函数中的代码(为这一个新对象添加属性和格局);

04,再次来到新目的。

在前头例子的末段,person1和person2分级保存着Person的多少个不等的实例。那多个对象都有二个constructor(构造函数)属性,该属性指向Person,如下所示:

alert(person1.constructor === Person); //true
alert(person2.constructor === Person); //true

 对象的constructor属性最初是用来标识对象类型的。可是,提到检查和测试对象类型,依然instanceof操作符更可信赖一些。大家在这些事例中开创的装有指标既是Object的实例,同时也是Person的实例。这点通过instanceof操作符能够赢得验证:

alert(person1 instanceof Object); //true
alert(person1 instanceof Person); //true
alert(person2 instanceof Object); //true
alert(person2 instanceof Person); //true

 成立自定义的构造函数意味着现在得以将它的实例标识为一种特定的项目,而那多亏构造函数方式胜过工厂格局的位置。在这些事例中,person1和person2之所以同时是Object的实例,是因为兼具目的均一连自Object(详细内容稍后切磋)。

(以那种方法定义的构造函数是概念在Global对象(在浏览器中是window对象)中的,由此唯有另有认证,instanceof操作符和constructor属性始终会就算是在大局功能域中询问构造函数,后边会详细座谈浏览器对象模型BOM)。

1.2.1将构造函数当作函数

构造函数与其他函数的绝无仅有差别,就在于调用它们的点子差别。不过,构造函数终究也是函数,不设有定义构造函数的尤其语法。任何函数,只要经过new操作符来调用,那它就能够视作构造函数,而任何函数,尽管不经过new操作符来调用,那它与经常函数也不会有哪些两样。例如,前边例子中定义的Person()函数能够透过下列任何一种艺术来调用:

//当构造函数调用
var person = new Person('Nicholas', 29, 'Software Engineer');
person.sayName(); //"Nicholas"

//作为普通函数调用
Person('Greg', 27, 'Doctor'); //添加到window
window.sayName(); //"Greg"

//在另一个对象的作用域中调用
var o = new Object();
Person.call(o, 'Kristen', 25, 'Nurse');
o.sayName(); //"Kristen"

其一例子中的前两行代码呈现了构造函数的第一名用法,即选取new操作符来成立三个新目的。接下来的两行代码浮现了在不行使new操作符调用Person()会师世哪些结果:属性和格局都被添加给window对象了。当在全局成效域中调用一个函数时,this对象总是指向Global对象(在浏览器中正是window对象)。因而,在调用完函数之后,能够经过window对象来调用sayName()方法,并且还回去了”Greg”。最后,也足以选择call()(只怕apply())在有个别特殊对象的功效域中调用Person()函数。那里是在指标o的成效域中调用的,因而调用后o就具备了有着属性和sayName()方法。

1.2.2构造函数的难点

构造函数方式尽管好用,但也不用没有缺陷。使用构造函数的要害难点,正是各类方法都要在各类实例上再也创建一回。在前头的例证中,person1和person2都有二个sayName()的措施,但那八个主意不是同贰个Function的实例。不要忘了——ECMAScript中的函数是指标,由此每定义三个函数,约等于实例化了1个目的。从逻辑角度讲,此时的构造函数也得以如此定义:

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = new Function('alert(this.name)'); //与声明函数在逻辑上是等价的
}

 从那一个角度上来看构造函数,更易于精通每一种Person实例都蕴涵3个见仁见智的Function实例(以呈现name属性)的面目。如前所述,那七个函数是不等于的,下边包车型地铁代码能够表达这点:

alert(person1.sayName == person2.sayName); //false

而是,成立八个成功同样职分的Function实例的确没有须要,况且有this对象在,根本不用在举办代码前就把函数绑定到一定目的方面。由此,大可像上面那样,通过把函数定义转移到构造函数外部来缓解那个题材:

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = sayName;
}

function sayName() {
    alert(this.name);
}

var person1 = new Person('Nicholas', 29, 'Software Engineer');
var person2 = new Person('Greg', 27, 'Doctor');

person1.sayName();
person2.sayName();

alert(person1 instanceof Object); //true
alert(person1 instanceof Person); //true
alert(person2 instanceof Object); //true
alert(person2 instanceof Person); //true

alert(person1.sayName === person2.sayName); //true

 在那些事例中,我们把sayName()函数的概念转移到了构造函数外部。而在构造函数内部,大家将sayName属性设置成等于全局的sayName函数,这样一来,由于sayName包涵的是一个针对函数的指针,由此person1和person2对象就共享了在大局成效域中定义的同八个sayName()函数,那样狠抓在化解了三个函数做相同件事的标题,但是新题材又来了:在全局成效域中定义的函数实际上只能被有个别对象调用,那让全局效能域有点徒有其名。而更令人不或然承受的是,假诺指标急需定义很多方式,那么就要定义很多个构造函数,于是大家那些自定义的引用类型就丝毫未曾封装性可言了。辛亏,那些难点能够利用原型情势来消除。

1.3原型格局

作者们创造的每1个函数都有一个prototye(原型)属性,这特性情是八个指南针,指向一个对象,而这几个目的的用处是带有能够由特定项目标富有实例共享的属性和方法。倘若依据字面意思来精晓,那么prototype正是因而调用构造函数而创设的相当目的实例的原型对象。使用原型对象的便宜是足以让全部指标实例共享它所涵盖的本性和艺术。换句话说,不必在构造函数中定义对象实例的新闻,而是能够将那些新闻从来抬高到原型对象中,如下边包车型地铁例证所示:

function Person() {}

Person.prototype.name = 'Nicholas';
Person.prototype.age = 29;
Person.prototype.job = 'Software Engineer';
Person.prototype.sayName = function() {
    alert(this.name);
};

var person1 = new Person();
person1.sayName(); //"Nicholas"

var person2 = new Person();
person2.sayName(); //"Nicholas"

alert(person1.sayName === person2.sayName); //true

 在此,大家将sayName()方法和颇具属性直接抬高到了Person的prototype属性中,构造函数变成了空函数。即便那样,也还是能够经过调用构造函数来创设新对象,而且新对象还会有所相同的质量和措施。但与构造函数方式不相同的是,新对象的这个属性和情势是由具有实例共享的。换句话说,person1和person2访问的都以同等组属性和同二个sayName()函数。要知道原型形式的做事规律,必须先精通ECMAScript中原型对象的品质。

1.3.1知道原型对象

无论是怎么时候,只要创立了贰个新函数,就会依照一组特定的平整为该函数创设1个prototype属性,那一个天性指向函数的原型对象。在暗中认可景况下,全体原型对象都会活动获得三个constructor(构造函数)属性,这么些个性包蕴1个针对性prototype属性所在函数的指针。就拿前边的事例来说,Person.prototype.constructor指向Person。而经过那些构造函数,大家还可再三再四为原型对象添加任何属性和章程。

创建了自定义的构造函数之后,其原型对象暗许只会收获constructor属性,至于别的方法,则都是从Object继承而来的。当调用构造函数创立三个新实例后,该实例的在那之中将包蕴3个指针(内部属性),指向构造函数的原型对象。在无数贯彻中,那么些里面属性的名字是__proto__,而且通过脚本可以访问到,而在其余实现中,这么些脾性对剧本则是截然不可知的。可是,要领悟的真的关键的某个,正是这些三番五次存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间。

以往面使用Person构造函数和Person.prototype成立实例的代码为例,下图显示了逐条对象之间的涉嫌:

ECMAScript 1

 上海教室显示了Person构造函数、Person原型属性以及Person现有的多个实例之间的涉及。在此,Person.prototype指向了原型对象,而Person.prototype.constructor又指回了Person。原型对象中除了含有constructor属性之外,还包涵后来添加的其余质量。Person的各样实例——person1和person2都包罗二个里边属性,该属性仅仅针对了Person.prototype;换句话说,它们与构造函数没有直接的关系。其余,要丰裕留意的是,纵然这么些实例都不分包属性和方法,但大家却能够调用person1.sayName()。那是经过搜索对象属性的进度来达成的。

就算在好几达成中不或许访问到中间的__proto__属性,但在全部完毕中都能够因此isPrototypeOf()方法来分明指标时期是或不是存在那种涉及。从实质上讲,假设指标的__proto__针对调用isPrototypeOf()方法的指标(Person.prototype),那么这几个措施就重返true,如下所示:

alert(Person.prototype.isPrototypeOf(person1)); //true
alert(Person.prototype.isPrototypeOf(person2)); //true

 那里,我们用原型对象的isPrototypeOf()方法测试了person1和person2.因为它们的在那之中都有三个针对Person.prototype的指针,由此都回去了true.

每当代码读取有些对象的某部属性时,都会履行3次搜索,指标是具备给定名字的质量。搜索首先从指标实例本人开端,假若在实例中找到了具备给定名字的习性,则赶回该属性的值,假如没有找到,则继续寻找指针指向的原型对象,在原型对象中检索具有给定名字的性质。假诺在原型对象中找到了这几个天性,则赶回该属性的值。也正是说,在大家调用person1.sayName()的时候,会先后实施五次搜索,再问:“person1的原型有sayName属性吗?”答:“有。”于是,它就读取那二个保存在原型对象中的函数。当我们调用person2.sayName()时,将会复出相同的查找进程,获得一致的结果。而那多亏八个对象实例共享原型所保存的品质和格局的基本原理。(前边提到过,原型最初只含有constructor属性,而该属性也是共享的,因而能够通过对象实例访问)

尽管可以经过对象实例访问保存在原型中的值,但却无法透过对象实例重写原型中的值。倘若我们在实例中添加了贰脾性质,而该属性与实例原型中的八个脾性同名,这大家就在实例中创制该属性,该属性将会遮掩原型中的那多少个属性,来看上边包车型大巴例证:

function Person() {}

Person.prototype.name = 'Nicholas';
Person.prototype.age = 29;
Person.prototype.job = 'Software Engineer';
Person.prototype.sayName = function() {
    alert(this.name);
};

var person1 = new Person();
var person2 = new Person();

person1.name = 'Greg';
alert(person1.name); //"Greg"——来自实例
alert(person2.name); //"Nicholas"——来自原型

 在这么些事例中,person1的name被1个新值给挡住了。但随便访问person1.name依旧访问person2.name都能够健康地重回值,即分别是”格雷戈”(来自指标实例)和“Nicolas”(来自原型)。当在alert()中走访person1.name时,要求读取它的值,因而就会在这么些实例上探寻2个名为name的属性。那个脾性确实存在,于是就再次来到了它的值而毋庸再找找原型了。当以同样的格局访问person2.name时,并不曾在实例上发现该属性,由此就会继续查找原型,结果在那里找到了name属性。

当为指标实例添加叁天性格时,这些性情就会遮掩原型对象中保存的同名属性,换句话说,添加那么些天性只会阻拦大家访问原型中的那三个属性,但不会修改十三分属性。即便将以此性情设置为null,也只会在实例中设置那些天性,而不会上升其针对性原型的总是。但是,使用delete操作符则足以完全除去实例属性,从而能够让我们能够再次访问原型中的属性,如下所示:

function Person() {}

Person.prototype.name = 'Nicholas';
Person.prototype.age = 29;
Person.prototype.job = 'Software Engineer';
Person.prototype.sayName = function() {
    alert(this.name);
};

var person1 = new Person();
var person2 = new Person();

person1.name = 'Greg';
alert(person1.name); //"Greg"
alert(person2.name); //"Nicholas"

delete person1.name;
alert(person1.name); //"Nicholas"——来自原型

 在那几个修改后的例子中,大家选择delete操作符删除了person1.name,在此之前它保存的”格雷戈”值屏蔽了同名的原型属性。把它删除未来,就过来了对原型中name属性的连年。由此,接下去再调用person1.name时,重临的正是原型中的name属性的值了。

使用hasOwnProperty()方法能够检查和测试壹性子格是存在于实例中,依旧存在于原型中。这么些办法(不要忘了它是从Object继承来的)只在给定属性存在于对象实例中,才会重临true。来看上边包车型客车例子:

function Person() {}

Person.prototype.name = 'Nicholas';
Person.prototype.age = 29;
Person.prototype.job = 'Software Engineer';
Person.prototype.sayName = function() {
    alert(this.name);
};

var person1 = new Person();
var person2 = new Person();

alert(person1.hasOwnProperty('name')); //false

person1.name = 'Greg';
alert(person1.name); //"Greg"——来自实例
alert(person1.hasOwnProperty('name')); //true

alert(person2.name); //"Nicholas"——来自原型
alert(person2.hasOwnProperty('name')); //false

delete person1.name;
alert(person1.name); //"Nicholas"——来自原型
alert(person1.hasOwnProperty('name')); //false

 通过选用hasOwnProperty()方法,曾几何时访问的是实例属性,哪天访问的是原型属性就清楚了。调用person1.hasOwnProperty(‘name’)时,唯有当person1重写name属性后才会回到true。因为唯有这时候name才是2个实例属性,而非原型属性。下图展现了上边例子中在分歧景观下的落到实处与原型的涉嫌(为了不难时期,图中简易了与Person构造函数的关联)

ECMAScript 2

1.3.2原型与in操作符

有三种艺术采取in操作符:单独选拔和在for-in循环中接纳。在单独使用时,in操作符会在通过对象能够访问给定属性时回来true。无论该属性存在于实例中如故原型中。看一看下边包车型客车事例:

function Person() {}

Person.prototype.name = 'Nicholas';
Person.prototype.age = 29;
Person.prototype.job = 'Software Engineer';
Person.prototype.sayName = function() {
    alert(this.name);
};

var person1 = new Person();
var person2 = new Person();

alert(person1.hasOwnProperty('name')); //false
alert('name' in person1); //true

person1.name = 'Greg';
alert(person1.name); //"Greg"——来自实例
alert(person1.hasOwnProperty('name')); //true
alert('name' in person1); //true

alert(person2.name); //"Nicholas"——来自原型
alert(person2.hasOwnProperty('name')); //false
alert('name' in person2); //true

delete person1.name;
alert(person1.name); //"Nicholas"——来自原型
alert(person1.hasOwnProperty('name')); //false
alert('name' in person1); //true

 在上述代码执行的全部经过中,
name属性要么是直接在对象上访问到的,要么是由此原型访问到的。因而,调用”name”
in person始终都回来true。无论该属性存在于实例中依旧存在于原型中。同时利用hasOwnProperty和in操作符,就能够规定该属性到底是存在于对象中,如故存在于原型中。如下所示:

function hasOwnProperty(object, name) {
    return !object.hasOwnProperty(name) && (name in object);
}

 由于in操作符只要经过对象能够访问到属性就回到true,hasOwnProperty()只在品质存在于实例中才回到true,由此假诺in操作符重返true而hasOwnProperty()再次来到false,就足以明确属性是原型中的属性。上面来看一看下面定义的函数hasOwnProperty()的用法:

function Person() {}

Person.prototype.name = 'Nicholas';
Person.prototype.age = 29;
Person.prototype.job = 'Software Engineer';
Person.prototype.sayName = function() {
    alert(this.name);
};

function hasOwnProperty(object, name) {
    return !object.hasOwnProperty(name) && (name in object);
}

var person = new Person();
alert(hasOwnProperty(person, 'name')); //true

person.name = 'Greg';
alert(hasOwnProperty(person, 'name')); //false

 在此间,name属性先是存在于原型中,由此hasOwnProperty()再次回到true,当在实例中重写name属性后,该属性就存在于实例中了,因而hasOwnProperty()重回false。

在利用for-in循环时,再次来到的是享有能够通过对象访问的、可枚举的(enumerated)属性,当中既包蕴存在于实例中的属性,也囊括存在于原型中的属性,屏蔽了原型中比比皆是属性(即设置了[[DontEnum]]标记的性质)的实例属性也会在for-in循环中回到。因为依据规定,全体开发人士定义的性格都以可枚举的——唯有IE例外。

ECMAScript,IE的JScript实现中存在一个bug,即屏蔽数不胜数属性的实例属性不会并发在for-in循环中。例如:

var o = {
    toString: function() {
        return 'My Object';
    }
}

for (var prop in o) {
    if (prop == 'toString') {
        alert('Found toString'); //在IE中不会显示
    }
}

当以上代码运转时,应该会来得1个警示框,表明找到了toString()方法。那里的对象o定义了3个名为toString()的艺术,该办法屏蔽了原型中(不可胜计)的toString()方法。在IE中,由于其促成认为原型的toString()方法被打上了[[DontEnum]]标志就活该跳过该属性,结果我们就不会看出警告框。该bug会影响私下认可不可胜举的兼具属性和方式,包罗:hasOwnProperty()、propertyIsEnumerable()、toLocaleString()、toString()和valueOf()。有的浏览器也为constructor和prototype属性打上了[[DontEnum]]标志,但那并不是具备浏览器联合的做法。

1.3.2 更简便的原型做法

读者大致注意到了,前边例子中每添加1本品质和措施就要敲2回person.prototype,为减弱不要求的输入,也为了从视觉上更好地卷入原型的作用,更常见的做法是用叁个饱含全数属性和办法的目的字面量来重写整个原型对象,如上边包车型大巴事例所示:

person.prototype = {
    name: 'Nicholas',
    age: 29,
    job: 'Software Engineer',
    sayName: function() {
        alert(this.name);
    }
};

 在上边的代码中,大家将person.prototype设置为等于2个以指标字面量形式创立的新对象。最终结果一致,但有1个分歧:constructor属性不再指向Person了。前边介绍过,每创造多少个函数,就会同时创建它的prototype对象,那些目标也会自动获得constructor属性。而我们在此间运用的语法,本质上完全重写了暗许的prototype对象,因而constructor属性也就改成了新对象的constructor属性(指向Object构造函数),不再指向Person函数。此时,纵然instanceof操作符还是能再次来到正确的结果,但透过constructor已经力不从心明确指标的类别了,如下所示:

function Person() {}

Person.prototype = {
    name: 'Nicholas',
    age: 29,
    job: 'Software Engineer',
    sayName: function() {
        alert(this.name);
    }
};

var person = new Person();

alert(person instanceof Object); //true
alert(person instanceof Person); //true
alert(person.constructor === Object); //true
alert(person.constructor == Person); //false

 在此,用instanceof操作符测试Object和Person照旧再次回到true,但constructor属性则等于Object而不对等Person了。如若constructor值真的很重要,能够像上面那样专门将它设置回适当的值:

function Person() {}

Person.prototype = {
    constructor: Person,
    name: 'Nicholas',
    age: 29,
    job: 'Software Engineer',
    sayName: function() {
        alert(this.name);
    }
};

var person = new Person();

alert(person instanceof Object); //true
alert(person instanceof Person); //true
alert(person.constructor == Object); //false
alert(person.constructor == Person); //true

 以上代码特意包涵了1个constructor属性,并将它的值设置为Person,从而确认保障了该属性能够访问到适当的值。

1.3.4 原型的动态性

出于在原型中查找值的长河是贰次搜索,由此大家对原型对象所做的别的修改都能够即时从实例上反映出去——尽管是先创立了实例后修改原型也是这般,请看上边包车型客车例证:

function Person() {}

var person = new Person();

Person.prototype.sayHi = function() {
    alert('hi');
};

person.sayHi(); //"hi" (没有问题!)

 以上代码先成立了Person的一个实例,并将其保存在person中。然后,下一条语句在Person.prototype中添加了八个方法sayHi()。就算person实例是在丰硕新章程在此之前创设的,但它还是能够访问那一个新措施。其原因能够归纳为实例与原型之间的麻痹马虎连接关系。当我们调用person.sayHi()时,首先会在实例中找寻名为sayHi的习性,在没找到的情景下,会三番伍遍寻找原型,因为实例与原型之间的连天只然则是1个指针,而非一个副本,因而就足以在原型中找到新的sayHi属性并赶回保存在那里的函数。

固然能够每225日为原型添加属性和方法,并且修改能够及时在装有目的实例中反映出去,但只若是重写整个原型对象,那么情状就不雷同了。大家了然,调用构造函数时会为实例添加多个对准最初原型的__proto__指南针,而把原型修改为另叁个目的就约等于隔开分离了构造函数与最初原型之间的维系,请记住,实例中的指针仅针对原型,而不指向构造函数,请看上边3个比较的事例:

function Person() {}

Person.prototype = {
    constructor: Person,
    name: 'Nicholas',
    age: 29,
    job: 'Software Engineer',
    sayName: function() {
        alert(this.name);
    }
};

var person = new Person();

person.sayName(); //"Nicholas" 

function Person() {}

var friend = new Person();

Person.prototype = {
    constructor: Person,
    name: 'Nicholas',
    age: 29,
    job: 'Software Engineer',
    sayName: function() {
        alert(this.name);
    }
};

friend.sayName(); //VM319:15 Uncaught TypeError: person.sayName is not a function(…)

在第三个例子中,大家先是创制了Person的三个实例,然后又重写了其原型对象,然后在调用Person.sayName()时发出了不当,因为person指向的原型中不含有以化名字命名的属性,下图突显了这几个进度的底子:

ECMAScript 3

上图中能够见到,重写原型对象切断了现有原型与其余在此之前早已存在的靶子实例之间的关系。它们引用的照旧是最初的原型。

1.3.5 原生对象的原型

原型情势的显要不仅呈以后创制自定义类型方面,
就连具有原生的引用类型,都以行使那种情势创立的。全数原生引用类型(Object、Array、String等)都在其构造函数的原型上定义了法子。例如,在Array.prototype中得以找到sort()方法,而在String.prototype中能够找到subString()方法,如下所示:

alert(typeof Array.prototype.sort); //"function"
alert(typeof String.prototype.substring); //"function"

由此原生对象的原型,不仅能够博得具有暗许方法的引用,而且也得以定义新措施。能够像修改自定义对象的原型一样修改原生对象的原型,因而能够天天添加方法。下边的代码就给大旨包装档次String添加了叁个名为startsWith()的方法:

String.prototype.startsWith = function(text) {
    return this.indexOf(text) == 0;
};

var msg = 'Hello world';
alert(msg.startsWith('Hello')); //true

此间新定义的starsWith()方法会在流传的公文位于三个字符串起先时再次回到true。既然方法被添加给了String.prototype,那么当前条件中的全部字符串就都得以调用它。由于msg是字符串,而且后台会调用String基本包装函数创立这些字符串,因而通过msg就足以调用startsWith()方法。(虽然可以这么做,不过不引进在产品化的程序中期维修改原生对象的原型。假设因有些达成中不够某些方法,就在原生对象的原型中添加那么些点子,那么当在另一个扶助该方法的兑现中运作代码时,就恐怕会促成命名争持。而且,那样做也恐怕会奇怪地重写原生方法。)

1.3.6 原型对象的标题

原型情势也不是从未缺陷,首先,它回顾了为构造函数发轫化参数这一环节,结果有所实例在暗许意况下都将收获一致的属性值。即使那会在某种程度上带来一些不便于,但还不是原型的最大难题,原型形式的最大难题是由其共享的本性所造成的。

原型中兼有属性是被过多实例共享的,那种共享对于函数卓殊适宜。对于那么些含有基本值的本性倒也说的过去,终究(如前方的例子所示),通过在实例上添加贰个同名属性,能够隐藏原型中的对应属性,可是,对于富含引用类型值的性质来说,难点就比较优良了,来看上边的例子:

function Person() {}

Person.prototype = {
    constructor: Person,
    name: 'Nicholas',
    age: 29,
    job: 'Software Engineer',
    friends: ['Shelby', 'Court'],
    sayName: function() {
        alert(this.name);
    }
};

var person1 = new Person();
var person2 = new Person();

person1.friends.push('Van');

alert(person1.friends); //"Shelby,Court,Van"
alert(person2.friends); //"Shelby,Court,Van"

在此,Person.prototype对象有三个名为friends的习性,该属性包括一个字符串数组,然后制造了Person的八个实例,接着,修改了person1.friends引用的数组,向数组中添加了八个字符串,由于friends数组存在于Person.prototype而非person1中,所以刚刚提到的修改也会通过person2.friends(与person1.friends指向同1个数组)反映出去。要是大家的初衷正是像那样在享有实例中国共产党享一个数组,那么对于那一个结果大家无话可说,但是,实例一般都以要有属于本身的整整天性的,而这一个题材正是大家很少见到有人单独行使原型情势的缘由所在。

1.4 组合使用构造函数方式和原型格局 

创办自定义类型的最常见方式,正是整合使用构造函数格局和原型格局。构造函数情势用于定义实例属性,而原型情势用于定义方法和共享的本性,结果,每一种实例都会有友好的一份实例属性的副本,但同时又共享着对章程的引用,最大限度地节约了内存。其余,这种混成格局还帮衬向构造函数字传送递参数,可谓是集二种形式之长,下边包车型客车代码重写了前方的事例:

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ['Shelby', 'Court'];
}

Person.prototype = {
    constructor: Person,
    sayName: function() {
        alert(this.name);
    }
};

var person1 = new Person('Nicholas', 29, 'Software Engineer');
var person2 = new Person('Greg', 27, 'Doctor');

person1.friends.push('Van');
alert(person1.friends); //"Shelby,Court,Van"
alert(person2.friends); //"Shelby,Court"
alert(person1.friends == person2.friends); //false
alert(person1.sayName == person2.sayName); //true

 在这些事例中,实例属性都以在构造函数中定义的,而由拥有实例共享的习性constructor和方法sayName()则是在原型中定义的,而修改了person1.friends(向当中添加了二个新字符串),并不会潜移默化到person2.friends,因为它们各自引用了差别的数组。

那种构造函数与原型混成的情势,是当前在ECMAScript中利用最普遍,认可度最高的自定义类型的法子。能够说,那是用来定义引用类型的一种私下认可情势。

1.5 动态原型格局

有其它OO语言经验的开发人士在阅览独立的构造函数和原型时,很恐怕会倍感非常纳闷。动态原型格局正是致力于化解那些标题标一个方案,它把持有消息都封装在了构造函数中,而透过在构造函数中先导化原型(仅在须求的图景下),又保持了并且使用构造函数和原型的优点。换句话说,能够经过检查有个别应该存在的不二法门是还是不是可行,来支配是不是要开始化原型。来看1个例子:

function Person(name, age, job) {

    //属性
    this.name = name;
    this.age = age;
    this.job = job;

    //方法
    if (typeof this.sayName != 'function') {
        Person.prototype.sayName = function() {
            alert(this.name);
        };
    }
}

var person = new Person('Nicholas', 29, 'Software Engineer');
person.sayName(); //"Nicholas"

只顾构造函数中的方法上面包车型大巴if语句,那里只在sayName()方法不设有的气象下,才会将它添加到原型中。这段代码只会在第壹调用构造函数时才会实施。此后,原型已经形成初叶化,不必要再做什么修改了。可是要切记,这里对原型所做的修改,能够登时在装有实例中赢得显示。由此,那种艺术确实能够说不行周密。当中,if语句检查的能够是开始化之后应该留存的任何性质和方法——不必用一大堆if语句检查各类属性和办法,只要检查在那之中三个即可。对于使用那种方式创建的对象,还足以使用instanceof操作符鲜明它的花色。(使用动态原型情势时,无法选取对象字面量重写原型。前边早已说明过了,若是在曾经创立实例的境况下重写原型,那么就会切断现有实例与新原型之间的维系)

1.6 寄生构造函数方式

常常,在详谈的两种情势都不适用的情事下,能够运用寄生(parasitic)构造函数方式。那种情势的主导思想是创制1个函数,该函数的成效只是是包裹创造对象的代码,然后再回到新创建的对象,但从表面上看,这些函数又很像是典型的构造函数,下边是一个例证:

function Person(name, age, job) {
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function() {
        alert(this.name);
    };
    return o;
}

var person = new Person('Nicholas', 29, 'Software Engineer');
person.sayName(); //"Nicholas"

在那几个例子中,Person函数创立了多少个新对象,并以相应的性质和措施开头化该指标,然后又回来了这些指标。除了行使new操作符并把利用的包装对象叫做构造函数之外,那么些方式跟工厂情势其实是一摸一样的,构造函数在不再次来到值的场合下,暗中同意重返新目的实例。而通过在构造函数的末尾添加二个return语句,能够重写调用构造函数时回来的值。

本条情势能够在卓殊的事态下用来为对象制造构造函数。假诺大家想创设三个全部额外措施的独特数组,由于不可能一向修改Array构造函数,由此得以选用那些格局:

function SpecialArray() {

    //创建数组
    var values = new Array();

    //添加值
    values.push.apply(values, arguments);

    //添加方法
    values.toPipedString = function() {
        return this.join('|');
    };

    //返回数组
    return values;
}

var colors = new SpecialArray('red', 'blue', 'green');
alert(colors.toPipedString()); //"red|blue|green"

alert(colors instanceof SpecialArray); //false
alert(colors instanceof Object); //true
alert(colors.constructor == SpecialArray); //false

在那几个例子中,大家创设了二个叫SpecialArray的构造函数,在那么些函数内部,首先创立了四个数组,然后push()方法(用构造函数接收到的持有参数)发轫化了数组的值。随后,又给数组实例添加了多少个toPipedString()方法,该办法重临以竖线分隔的数组值。最后,将数组以函数值的款式重回。接着,我们调用了 SpecialArray构造函数,向在那之中传入了用于开始化数组的值,此后又调用了toPipedString()方法。

至于寄生构造函数形式,有某个急需证实:首先,重返的目的与构造函数也许与构造函数的原型属性之间从未涉及,也正是说,构造函数重回的靶子与在构造函数外部创制的对象没有怎么区别,为此,无法重视instanceof操作符来明显指标类型。由于存在上述难题,我们建议在能够选择任何形式的气象下,不要选用那种形式。

1.7 稳妥构造函数情势

妥贴对象(durable
objects),指的是平素不国有属性,而且其艺术也不引用this的目的。稳妥对象最契合在一些有惊无险的条件中(那一个环境中会禁止行使this和new),大概在防止数据被其它应用程序(如Mashup程序)改动时利用。稳当构造函数遵守与寄生构造函数类似的方式,但有两点不一样:一是新创立对象的实例方法不引用this;二是不选用new操作符调用构造函数。依据妥贴构造函数的要求,能够将前方的Person构造函数重写如下:

function Person(name, age, job) {

    //创建要返回的对象
    var o = new Object();

    //可以在这里定义私有变量和函数

    //添加方法
    o.sayName = function() {
        alert(name);
    };

    //返回对象
    return o;
}

var person = Person('Nicholas', 29, 'Software Engineer');
person.sayName(); //"Nicholas"

alert(person instanceof Person); //false
alert(person instanceof Object); //true
alert(person.constructor == Person); //false

留意,在以那种格局开创的指标中,除了行使sayName()方法之外,没有其余办法访问name的值。变量person中保存的是1个妥帖对象,而除去调用sayName()方法外,没有别的艺术能够访问其他数据成员。即使有任何代码会给那几个目的添加方法或数量成员,但也不容许有其他办法访问传入到构造函数中的原始数据。得当构造函数形式提供的那种安全性,使得它非凡适合在某个安全实施环境——例如,ADsafe(www.adsafe.org)和Caja(http://code.google.com/google-caja/)提供的环境下使用。(与寄生构造函数模式类似,使用稳妥构造函数模式创建的对象与构造函数之间也没有什么关系,因此instanceof操作符对这种对象也没有意义)

2,继承

继承是OO语言中的3个无比人津津乐道的定义。许多OO语言都援助二种持续方式:接口继承和完毕再三再四。接口继承只持续方法签名,而达成一而再则持续实际的章程。如前所述,由于函数没有签定,在ECMAScript中不能兑现接口继承。ECMAScript只帮忙促成持续,而且其促成三番五次主假使凭借原型链来达成的。

2.1 原型链

ECMAScript中讲述了原型链的定义,并将原型链作为完毕持续的主要性方法。其主干思想是使用原型让叁个引用类型继承另一个引用类型的习性和格局。不难回想一下构造函数、原型和实例的涉及:每一个构造函数都有3个原型对象,原型对象都包罗贰个对准构造函数的指针,而实例都含有一个针对性原型对象的个中指针。那么,假使我们让原型对象等于另二个品类的实例,结果会什么呢?显明,此时的原型对象将含有贰个针对性另八个原型的指针,相应地,另3个原型中也暗含着2个针对性另三个构造函数的指针。倘使另八个原型又是另3个类型的实例,那么上述提到依然创建,如此罕见递进,就构成了实例与原型的链子。那就是所谓原型链的概念。

兑现原型链有一种基本形式,其代码大约如下:

function SuperType() {
    this.property = true;
}

SuperType.prototype.getSuperValue = function() {
    return this.property;
};

function SubType() {
    this.subproperty = false;
}

//继承了SuperType
SubType.prototype = new SuperType();

SubType.prototype.getSubValue = function() {
    return this.subproperty;
};

var instance = new SubType();
alert(instance.getSuperValue()); //true

alert(instance instanceof Object); //true
alert(instance instanceof SuperType); //true
alert(instance instanceof SubType); //true

alert(Object.prototype.isPrototypeOf(instance)); //true
alert(SuperType.prototype.isPrototypeOf(instance)); //true
alert(SubType.prototype.isPrototypeOf(instance)); //true

上述代码定义了多个连串:SuperType和SubType 。每一个项目分别有2个属性和2个方式。它们的重要差别是SubType继承了SuperType,而后续是经过创办SuperType的实例,并将该实例赋给SubType.prototype达成的,完成的面目是重写原型对象,代之以三个新类型的实例。换句话说,原来存在于SuperType的实例中的全体属性和办法,以后也存在于SubType.prototype中了,在建立了继承关系随后,大家给SubType.prototype添加了3个方式,那样就在再三再四了SuperType的习性和方法的功底上又添加了贰个新措施。那一个事例中的实现以及构造函数与原型之间的涉及如下图所示:

ECMAScript 4

在地点的代码中,大家向来不使用SubType默许提供的原型,而是给它换了二个新原型:这些新原型正是SuperType的实例。于是,新原型不仅有着作为四个SuperType的实例所具备的整整属性和方法,而且其里面还有二个指针,指向了SuperType的原型。最后结出正是这么的:instance指向SubType的原型,SubType的原型又指向SuperType的原型。getSuperValue()方法依旧还在SuperType.prototype中,但property则放在SubType.prototype中,那是因为property是二个实例属性,而getSuperValue()则是3个原型方法。既然SubType.prototype以往是SuperType的实例,那么property当然就置身该实例中了。其余,需注意instance.constructor现在于今本着的是SuperType,那是因为原先的SubType.prototype中的constructor被重写了的由来(实际上,不是SubType的原型的constructor属性被重写了,而是SubType的原型指向了另八个指标——SuperType的原型,而这些原型对象的constructor属性指向的是SuperType。)

由此落实原型链,本质上扩张了前头介绍的原型搜索机制。当以读取格局访问2个实例属性时,首先会在实例中找找该属性,假若没有找到该属性,则会持续寻找实例的原型。在通过原型链达成几次三番的动静下,搜索进程就足以沿着原型链继续开拓进取。就拿地点的事例来说,调用instance.getSuperValue()会经历八个搜索步骤:1)搜索实例;2)搜索SubType.prototype;3)搜索SuperType.prototype,最终一步才会找到该办法。在找不到属性和艺术的意况下,搜素进度接连要一环一环地前行到原型链末端才会停下来。

2.1.1 别忘记私下认可的原型

实在,前边例子中显得的原型链还少一环。大家领略,全数引用类型暗中认可都三番五次了Object,而这些接二连三也是经过原型链来达成的。我们要切记,全体函数的暗中认可原型都以Object的实例,由此默许原型都会包涵一个里面指针,指向Object.prototype.那也多亏拥有自定义类型都会继续toString()、valueOf()等默许方法的根本原因。所以,大家说上面例子彰显的原型链中还相应包蕴别的3个即成层次,下图为大家来得了该例子中完全的原型链。

ECMAScript 5

一句话,SubType继承了SuperType,而SuperType继承了Object。当调用instance.toString()时,实际上调用的是保存在Object.prototype中的那些格局。

2.1.2 分明原型与实例的涉嫌

能够通过三种方法来分明原型和实例之间的关联。第叁种艺术是运用instanceof操作符,只要用这一个操作符来测试实例与原型链中出现过的构造函数,结果就会重返true。以下几行代码就表达了那或多或少:

alert(instance instanceof Object); //true
alert(instance instanceof SuperType); //true
alert(instance instanceof SubType); //true

由于原型链的关联,大家得以说instance是Object、 SuperType、SubType中此外一个类型的实例,因而,测试那四个构造函数的结果都回来了true.

第3种方法是应用isPrototypeOf()方法。同样,只即使原型链中出现过的原型,都得以说是该原型链所派生的实例的原型。因而isPrototypeOf()方法也会回去true。如下所示:

alert(Object.prototype.isPrototypeOf(instance)); //true
alert(SuperType.prototype.isPrototypeOf(instance)); //true
alert(SubType.prototype.isPrototypeOf(instance)); //true

2.1.3 谨慎地定义方法

子类型有时候需求重写超类型的某部方法,大概须要加上超类型中不设有的某部方法,但不论怎么,给原型添加方法的代码一定要放在替换原型的言辞之后,来看上面包车型客车事例:

function SuperType() {
    this.property = true;
}

SuperType.prototype.getSuperValue = function() {
    return this.property;
};

function SubType() {
    this.subproperty = false;
}

//继承了SuperType
SubType.prototype = new SuperType();

//添加新方法
SubType.prototype.getSubValue = function() {
    return this.subproperty;
};

//重写超类型中的方法
SubType.prototype.getSuperValue = function() {
    return false;
};

var instance = new SubType();
alert(instance.getSuperValue()); //false

以上代码中,插手色情背景的部分是多少个章程的概念。第二个措施getSubValue()被添加到了SubType中,第①个艺术getSuperValue()是原型链中已经存在的三个措施,但重写这几个艺术将会遮掩原来的很是格局。换句哈说,当通过SubType的实例调用getSuperValue()时,调用的正是以此重新定义的章程,但由此SubType的实例调用getSuperValue()时,还会一连调用原来的丰富格局。那里要求卓殊留意的是,必须在用SuperType的实例替换原型之后,再定义那多少个点子。

再有少数须求提醒读者,即在经过原型链完成延续时,无法应用对象字面量创制原型方法,因为这么做就会重写原型链,如上面包车型地铁例证所示:

function SuperType() {
    this.property = true;
}

SuperType.prototype.getSuperValue = function() {
    return this.property;
};

function SubType() {
    this.subproperty = false;
}

//继承了SuperType
SubType.prototype = new SuperType();

//使用字面量添加新方法,会导致上一行代码无效
SubType.prototype = {
    getSubValue: function() {
        return this.subproperty;
    },

    someOtherMethod: function() {
        return false;
    }
};

var instance = new SubType();
alert(instance.getSuperValue()); //VM627:1 Uncaught TypeError: instance.getSuperValue is not a function(…)

如上代码体现了正要把SuperType的实例赋值给原型,紧接着又将原型替换到一个指标字面量而致使的标题。由于前日的原型包蕴的是一个Object实例,而非SuperType的实例,由此大家着想中的原型链已经切断——SubType和SuperType之间业已远非关联了。

2.1.4 原型链的标题

原型链尽管很强劲,能够用它来兑现持续,但它也设有部分题材。在那之中,最首要的题材根源包蕴引用类型值的原型。想必大家还记得,大家眼下介绍过含有引用类型值的原型属性会被全数实例共享,而那也多亏为啥要在构造函数中,而不是在原型对象中定义属性的缘故。在经过原型达成持续时,原型实际上会成为另四个品种的实例,于是,原先的实例属性也就顺理成章地成为了以往的原型属性了。下边代码可以用来证实这几个题材:

function SuperType() {
    this.colors = ['red', 'blue', 'green'];
}

function SubType() {}

//继承了SuperType
SubType.prototype = new SuperType();

var instance1 = new SubType();
instance1.colors.push('black');
alert(instance1.colors); //"red,blue,green,black"

var instance2 = new SubType();
alert(instance2.colors); //"red,blue,green,black"

以此事例中的 SuperType构造函数定义了七个colors属性,该属性包罗多少个数组(引用类型值)。SuperType的种种实例都会有分别包括自身数组的colors属性。当SubType通过原型链继承了SuperType之后,SubType.prototype就改成了SuperType的2个实例,因而它也富有了二个它本人的colors属性——就跟专门创设了三个SubType.prototype.colors属性一样。但结果是何等吗?结果是SubType的保有实例都会共享那几个colors属性。而大家对instance1.colors的修改能够因而instance2.colors反映出去,就已经丰富表明了这点。

原型链的第三个难题是:在创制子类型的实例时,不能够向超类型的构造函数中传送参数。实际上,应该说是没有章程在不影响全数目的实例的情状下,给超类型的构造函数字传送递参数。有鉴于此,再加上前边刚钻探过的原型中包蕴引用类型值所带来的难题,实践中很少会独自使用原型链。

2.2 使用构造函数

在消除原型中含有引用类型值所推动难点的历程中,开发职员起初选取一种名叫借用构造函数(constructor
stealing)的技能(有时候也号称伪造对象或经典连续)。那种技术的主干考虑很是不难,即在子类型构造函数的里边调用超类型构造函数。别忘了,函数只然而是在一定执行环境中实施代码的指标,由此通过行使apply()和call()方法也能够在(现在)新创造的目的上进行构造函数,如下所示:

function SuperType() {
    this.colors = ['red', 'blue', 'green'];
}

function SubType() {
    //继承了SuperType
    SuperType.call(this);
}

var instance1 = new SubType();
instance1.colors.push('black');
alert(instance1.colors); //"red,blue,green,black"

var instance2 = new SubType();
alert(instance2.colors); //"red,blue,green"

代码中加背景的那一行代码
“借调”了超类型的构造函数。通过行使call()方法(或apply()方法也得以),大家其实是在(今后即将)新创制的SubType实例的条件下调用了SuperType构造函数。那样一来,就会在新SubType目的上实施SuperType()函数中定义的拥有目的开首化代码。结果,SubType的各类实例就都会有所本人的colors属性的副本了。

2.2.1 传递参数

争执于原型链而言,借用构造函数有二个非常的大的优势,即能够在子类型构造函数中向超类型构造函数字传送递参数。请看下边包车型大巴例证:

function SuperType(name) {
    this.name = name;
}

function SubType() {
    //继承了SuperType,同时还传递了参数
    SuperType.call(this, 'Nicholas');

    //实例属性
    this.age = 29;
}

var instance = new SubType();
alert(instance.name); //"Nicholas"
alert(instance.age); //29

 以上代码中的SuperType只接受3个参数name,该参数会直接赋给多少个性质。在SubType构造函数内部调用SuperType构造函数时,实际上是为SubType的实例设置了name属性。为了保障SubType构造函数不会重写子类型的属性,可用在调用超类型构造函数之后,再添加应该在子类型中定义的习性。

2.2.2 借用构造函数的难题

借使单单是借用构造函数,那么也将不可能幸免构造函数格局存在的题材——方法都在构造函数中定义,因而函数复用就无法谈起了。而且,在超类型的原型中定义的章程,对子类型而言也是不可知的,结果具有类型都不得不动用构造函数格局,考虑到这一个标题,借用构造函数的技艺也是很少单独选取的。

2.3 组合继承

结合继承(combination
inheritance),有时候也叫做伪经典一连,指的是将原型链和借用构造函数的技艺整合到一块,从而发挥而者之长的一种持续方式。其幕后的思绪是运用原型链达成对原型属性和办法的持续,而透过借用构造函数来落成对实例属性的后续。那样,既通过在原型上定义方法完成了函数复用,又能够保险各样实例都有它自身的属性。上面来看一个事例:

function SuperType(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}

SuperType.prototype.sayName = function() {
    alert(this.name);
};

function SubType(name, age) {
    //继承属性
    SuperType.call(this, name);

    this.age = age;
}

//继承方法
SubType.prototype = new SuperType();

SubType.prototype.sayAge = function() {
    alert(this.age);
};

var instance1 = new SubType('Nicholas', 29);
instance1.colors.push('black');
alert(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas"
instance1.sayAge(); //29

var instance2 = new SubType('Greg', 27);
alert(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg"
instance2.sayAge(); //27

在那些例子中,SuperType构造函数定义了七个性子,name和colors。SuperType的原型定义了3个措施sayName()。SubType构造函数在调用SuperType构造函数时传出了name参数,紧接着又定义了它自个儿的性质age。然后,将SuperType的实例赋值给SubType的原型,然后又在该新原型上定义了办法sayAge(),那样一来,就足以让多少个例外的SubType实例既分别拥有自个儿属性——包涵colors属性,又能够应用同样的办法了。

构成继承防止了原型链和借用构造函数的败笔,融合了他们的独到之处,成为JavaScript中最常用的持续情势。而且,instanceof和isPrototypeOf()也能够用于识别基于组合继承成立的指标。

2.4 原型式继承

DougRuss 克罗克福德在二〇〇五年写了一篇小说,题为Prototypal Inheritance in
JavaScript(JavaScript中的原型式继承)。在这篇文章中,他介绍了一种达成一而再的方式,那种格局并从未行使严谨意义上的构造函数。他的想法是凭借原型能够依据已有个别对象创设新对象,同时还不必因而成立自定义类型。为了达成那几个目标,他提交了如下函数:

function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}

在object()函数内部,先创制了二个暂且性的构造函数,然后将盛传的对象作为这一个构造函数的原型,最后回到了这么些权且类型的新实例。从本质上讲,object()对传播在那之中的靶子实行了一遍浅复制。来看上边包车型客车例子:

function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}

var person = {
    name: "Nicholas",
    friends: ['Shelby', 'Court', 'Van']
};

var anotherPerson = object(person);
anotherPerson.name = 'Greg';
anotherPerson.friends.push('Rob');

var yetAnotherPerson = object(person);
yetAnotherPerson.name = 'Linda';
yetAnotherPerson.friends.push('Barble');

alert(person.friends); //"Shelby,Court,Van,Rob,Barble"

克罗克福德主持的这种原型式继承,要求你必须有2个指标足以看成另2个对象的底子。若是有诸如此类叁个对象的话,能够把它传递给object()函数,
然后再依照实际须要对得到的指标加以修改即可。在那一个例子中,可用作为另1个函数基础的是person对象,于是我们把它传播到object()函数中,然后该函数就会再次来到1个新目的,那个新指标将person作为原型,所以它的原型中就含有1当中坚项目值属性和二个引用类型值属性。那意味着person.friends不仅属于person全体,而且也会被anotherPerson以及yetAnotherPerson共享。实际上,这就一定于再次创下办了person对象的八个副本。

在没有须求兴师动众地开创构造函数,而只想让三个对象与另多个指标保证类似的图景下,原型式继承是全然能够胜任的。可是别忘了,蕴含引用类型值的属性始终都会共享相应的值。就如使用原型格局一样。

2.5 寄生式继承

寄生式(parasitic)继承是与原型式继承紧凑有关的一种思路,并且相同也是由克罗克福德推而广之的。寄生式继承的思绪与构造函数和工厂格局类似,即开立1个仅用于封装继承进程的函数,该函数在里面以某种方式来进步对象,最终再像真就是它做了全部工作一样再次回到对象。以下代码示范了寄生式继承方式:

function createAnother(original) {
    var clone = object(original); //通过调用函数创建一个新对象
    clone.sayHi = function() { //以某种方式来增强这个对象
        alert('hi');
    };
    return clone; //返回这个对象
}

 在那些事例中,createAnother()函数接收了1个参数,也便是即将作为新目的基础的对象。然后,把那些指标(original)传递给object()函数,将赶回的结果赋值给clone,再为clone对象添加3个新情势sayHi(),最后回到clone对象。可用像上边包车型客车例证来使用createAnother()函数:

var person = {
    name: 'Nicholas',
    friends: ['Shelby', 'Court', 'Van']
};

var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //"hi"

 那些例子中的代码基于person再次回到了3个新对象——anotherPerson。新指标不仅具备person的具有属性和艺术,而且还有自身的sayHi()方法。

在根本考虑对象而不是自定义类型和构造函数的地方下,寄生式继承也是一种有效的情势。后边示范继承格局时采用的object()函数不是必须的。任何能够回来新目的的函数都适用于此情势。(使用寄生式继承来为对象添加函数,会由于不能够不负众望函数复用而下降功能,那或多或少与构造函数情势类似)

2.6 寄生组合式继承

前面说过,组合继承是JavaScript最常用的接二连三方式,不过,它也有温馨的缺乏。组合继承的难点就是无论什么动静下,都会调用三遍超类型构造函数:三回是在成立子类型原型的时候,另三遍是在子类型构造函数内部。没错,子类型最终会包括超类型对象的全方位实例属性,但我们只幸亏调用子类型构造函数时重写这几个属性。再来看一看下边组合继承的例证:

function SuperType(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}

SuperType.prototype.sayName = function() {
    alert(this.name);
};

function SubType(name, age) {
    SuperType.call(this, name); //第二次调用SuperType()

    this.age = age;
}

SubType.prototype = new SuperType(); //第一次调用SuperType()

SubType.prototype.sayAge = function() {
    alert(this.age);
};

 带靛蓝背景的行中是调用SuperType构造函数的代码。在第一回调用SuperType构造函数时,SubType.prototype会得到五个属性:name和colors;他们都是SuperType的实例属性,只不过未来位居SubType的原型中。当调用SubType构造函数时,又会调用贰遍SuperType构造函数,那三回又在新对象上创造了实例属性name和colors。于是,那七个属性就屏蔽了原型中的三个同名属性。下图呈现了上述进度:

ECMAScript 6

如上海教室所示,有两组name和colors属性:一组在实例上,一组在SubType原型中。这正是调用一回SuperType构造函数的结果。万幸咱们早就找到了缓解那个题材的办法——寄生组合式继承。

所谓寄生组合式继承,即因此借用构造函数来一而再属性,通过原型链的混成方式来连续方法。其幕后的基本思路是:不必为了内定子类型的原型而调用超类型的构造函数,我们所须要的只有就是超类型原型的1个副本而已。本质上,就是使用寄生式继承来三番五次超类型的原型,然后再将结果钦定给子类型的原型。寄生组合式继承的基本形式如下所示:

function inheritPrototype(subType, superType) {
    var prototype = object(superType, prototype); //创建对象
    prototype.constructor = subType; //增强对象
    subType.prototype = prototype; //指定对象
}

以此示例中的inheritPrototype()函数达成了寄生组合式继承的最简易款式。这些函数接收三个参数:子类型构造函数和超类型构造函数。在函数内部,第③步是创立超类型原型的2个副本。第三步是为创建的副本添加constructor属性,从而弥补因重写原型而错过的暗许的constructor属性。最终一步,将新创建的对象(即副本)赋值给子类型的原型。那样,大家就足以调用inheritPrototype()函数的语句,去替换前边例子中为子类型原型赋值的语句了,例如:

function SuperType(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}

SuperType.prototype.sayName = function() {
    alert(this.name);
};

function SubType(name, age) {
    SuperType.call(this, name); 
    this.age = age;
}

inheritPrototype(SubType, SuperType);

SubType.prototype.sayAge = function() {
    alert(this.age);
};

 那几个事例的高作用映未来它只调用一回SuperType构造函数,并且由此幸免了在SubType.prototype上创设不供给的、多余的质量。于此同时,原型链还能够有限支撑不变;由此还能够够平常使用instanceof和isPrototypeOf()。开发职员普遍认为寄生组合式继承是引用类型最完美的继续范式。(YUI的YAH.lang.extand()方法应用了寄生组合继承,从而让那种方式第四回出现在了二个施用广泛的JavaScript库中,要驾驭关于YUI的更加多音信,请访问http://developer.yahoo.com/yui)

小结:

ECMAScript接济面向对象(OO)编制程序,但不使用类只怕接口。对象足以在代码执行进度中开创和增加,由此具有动态性而非严峻定义的实体。在未曾类的图景下,能够使用下列格局创制对象:

a,工厂情势,利用简易的函数创设对象,为目的添加属性和方法,然后再次来到对象。这些情势后来被构造函数方式所代替。

b,构造函数形式,能够制造自定义引用类型,能够像创造内置对象实例一样使用new操作符,不过,构造函数情势也有通病,即它的各个成员都没办法儿获取复用,包罗函数。由于函数能够不局限于其余对象(即与目的具备松弛耦合的特性),因而尚未理由不在八个指标间共享函数。

c,原型方式,使用构造函数的prototype属性来钦定那个应该共享的属性和办法,组合使用构造函数方式和原型形式时,使用构造函数定义实例属性,而选择原型定义共享的性质和措施。

JavaScript重要通过原型链达成一而再,原型链的创设是通过将3个项指标实例赋值给另三个构造函数的原型完成的。那样,子类型就能够访问超类型的具有属性和措施,那或多或少与主干类型的接轨很相像。原型链的标题是指标实例共享全数继续的品质和方法,因而不妥帖单独使用。化解那么些题指标技艺是借用构造函数,即在子类型构造函数的在那之中调用超类型构造函数。那样就能够成功每种实例都富有友好的质量,同时仍能够确定保证只利用构造函数形式来定义类型。使用最多的持续方式是结合继承,那种情势应用原型链继承共享的本性和办法,而经过借用构造函数继承实例属性。

其它,还留存下列可供选用的持续形式:

a,原型式继承,可用在不必预先定义构造函数的情状下促成持续,其本质是履行对给定对象的浅复制,而复制获得的副本还足以拿走越来越改造。

b,寄生式继承,与原型继承格外相像,也是依照有个别对象或一些消息创立3个目的,然后增强对象,最终回到对象。为了化解组合继承情势由于反复调用超类型构造函数而导致的低功能难点,可用将以此格局与构成继承一起行使。

c,寄生组合式继承,集寄生式继承和组合继承的亮点于一身,是落到实处基于项目继承的最实用措施。